前言:近期项目重构,公司对项目结构进行了调整,增加了条件筛选的功能。在网上也看到了很多自定义的控件实现类似美团的附近筛选功能,实现方式基本都是自定义view继承自LinearLayout等布局控件,于是自己在Button的基础上添加了popupWindow并进行了简单封装,使用RecyclerView展示数据,实现了带二级菜单的筛选功能。
效果图:
实现方式:
(1)继承自AppCompatButton,实现PopupWindow.OnDismissListener
(2)PopupWindow展示二级菜单
(3)RecyclerView展示二级菜单的列表数据
(4)监听接口进行回调,用于筛选后更新UI
1.定义FilterPopupButton
(1)FilterPopupButton继承自AppCompatButton,重写构造方法
//三个有参构造方法 public FilterPopupButton(Context context) { super(context); this.context = context; } public FilterPopupButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; } public FilterPopupButton(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; inflater = LayoutInflater.from(context); initAttrs(context, attrs); initParams(context); }(2)设置控件属性参数
/** * 初始化属性参数 * @param context 上下文对象 * @param attrs 属性 */ private void initAttrs(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.popupbtn); //正常状态下的背景色 mBackGround = typedArray.getResourceId(R.styleable.popupbtn_normalBg, -1); //点击时的背景色 mClickBackGround = typedArray.getResourceId(R.styleable.popupbtn_pressBg, -1); //正常状态下的图标 mIcon = typedArray.getResourceId(R.styleable.popupbtn_normalIcon, -1); //点击状态下的图标 mClickIcon = typedArray.getResourceId(R.styleable.popupbtn_pressIcon, -1); //回收 typedArray.recycle(); }定义attrs,方便在布局文件使用时设置相关属性
<declare-styleable name="popupbtn"> <!-- 正常状态下的背景 --> <attr name="normalBg" format="reference" /> <!-- 点击状态下的背景 --> <attr name="pressBg" format="reference" /> <!-- 正常状态下的图标 --> <attr name="normalIcon" format="reference" /> <!-- 点击状态下的图标 --> <attr name="pressIcon" format="reference" />f </declare-styleable>(3)初始化其他参数
/** * 初始化参数 */ private void initParams(Context context) { //初始化图标的padding值 paddingTop = this.getPaddingTop(); paddingLeft = this.getPaddingLeft(); paddingRight = this.getPaddingRight(); paddingBottom = this.getPaddingBottom(); //获取Window管理器 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); //获取屏幕的宽和高 mScreenWidth = wm.getDefaultDisplay().getWidth(); mScreenHeight = wm.getDefaultDisplay().getHeight(); //设置正常状态下的参数 setNormalStatus(); }(4)设置正常状态及点击状态下的背景及图标
/** * 设置点击状态下的背景及图标 */ private void setClickStatus() { //设置点击时的背景色 if (mClickBackGround != -1) { this.setBackgroundResource(mClickBackGround); this.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); } //设置点击时的图标 if (mClickIcon != -1) { Drawable drawable = getResources().getDrawable(mClickIcon); // 设置drawable的bounds以便展示图标 drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight()); this.setCompoundDrawables(null, null, drawable, null); } } /** * 设置正常状态下的背景及图标 */ private void setNormalStatus() { //未点击状态背景 if (mBackGround != -1) { this.setBackgroundResource(mBackGround); this.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); } //未点击状态下的图标 if (mIcon != -1) { Drawable drawable = getResources().getDrawable(mIcon); // 设置drawable的bounds以便展示图标 drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight()); this.setCompoundDrawables(null, null, drawable, null); } }
2.PopupWindow
(1)初始化popupWindow布局文件
/** * 初始化popupWindow的布局文件 */ private void init() { //初始化popupWindow的布局文件 View view = inflater.inflate(R.layout.popup_layout, null); //展示一级菜单的view mRvParent = (RecyclerView) view.findViewById(R.id.rv_parent); //展示二级菜单的view mRvChild = (RecyclerView) view.findViewById(R.id.rv_child); //存储取值列表的集合 mValuesList = new ArrayList<>(); //默认添加一级菜单对应的二级菜单的第一个值列表 //mValuesList.addAll(mChildList.get(0)); mRvParent.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); mRvParent.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); mRvChild.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); mRvChild.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); //创建一级和二级菜单的adapter mParentAdapter = new FilterAdapter(context, R.layout.popup_item, mParentList); mChildAdapter = new FilterAdapter(context, R.layout.popup_item, mValuesList); //设置一级菜单的第一个默认选中状态 mParentAdapter.recordSelectPosition(0); mRvParent.setAdapter(mParentAdapter); mRvChild.setAdapter(mChildAdapter); //初始化popupWindow initPopupView(view); //父条目点击事件 mParentAdapter.setOnItemClickListener(new MultiItemTypeAdapter.OnItemClickListener() { @Override public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) { if (position == 0) { //position=0为筛选全部,点击popupWindow消失并设置取值为父条目的第一个值 setText(mParentList.get(position)); hidePopupWindow(mParentList.get(position)); } //记录父条目点击的position mParentAdapter.recordSelectPosition(position); mParentAdapter.notifyDataSetChanged(); mValuesList.clear(); //将子条目添加到取值列表 mValuesList.addAll(mChildList.get(position)); mChildAdapter.notifyDataSetChanged(); mChildAdapter.recordSelectPosition(-1); //设置子条目默认选中第一个 //mRvChild.scrollToPosition(0); } @Override public boolean onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) { return false; } }); //子条目点击事件 mChildAdapter.setOnItemClickListener(new MultiItemTypeAdapter.OnItemClickListener() { @Override public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) { //记录子条目点击的position mChildAdapter.recordSelectPosition(position); mChildAdapter.notifyDataSetChanged(); //设置选中的字符 setText(mValuesList.get(position)); //隐藏popupWindow hidePopupWindow(mValuesList.get(position)); } @Override public boolean onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) { return false; } }); }注:本文使用RecyclerView用于二级菜单数据列表的展示,并使用鸿洋大神封装的adapter,文末贴出依赖。
(2)popup_layout布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="horizontal"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_parent" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:scrollbars="none"> </android.support.v7.widget.RecyclerView> <View android:layout_width="1dp" android:layout_height="match_parent" android:background="@android:color/darker_gray" /> <android.support.v7.widget.RecyclerView android:id="@+id/rv_child" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"> </android.support.v7.widget.RecyclerView>
(3)initPopupWindow及hidePoupWindow
/** * 初始化popupWindow * @param view popupWindow的布局view */ public void initPopupView(final View view) { this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mFixPopupWindow == null) { LinearLayout layout = new LinearLayout(context); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) (mScreenHeight * 0.6)); view.setLayoutParams(params); layout.addView(view); //设置背景色,不设置的话在有些机型会不显示popupWindow layout.setBackgroundColor(Color.argb(60, 0, 0, 0)); //自定义的FixPopupWindow,解决在Build.VERSION.SDK_INT >= 24时,popupWindow显示位置在屏幕顶部问题 mFixPopupWindow = new FixPopupWindow(layout, mScreenWidth, mScreenHeight); mFixPopupWindow.setFocusable(true); mFixPopupWindow.setBackgroundDrawable(new BitmapDrawable()); //设置点击popupWindow外部可消失 mFixPopupWindow.setOutsideTouchable(true); mFixPopupWindow.setOnDismissListener(FilterPopupButton.this); layout.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mFixPopupWindow.dismiss(); } }); } //设置点击popupButton时的状态 setClickStatus(); mFixPopupWindow.showAsDropDown(FilterPopupButton.this); } }); }
/** * 隐藏popupWindow * @param value 获取到的值 */ public void hidePopupWindow(String value) { if (mFixPopupWindow != null && mFixPopupWindow.isShowing()) { popupButtonMonitor.setFilterResult(value); mFixPopupWindow.dismiss(); } }(4)定义recyclerView的adapter(FilterAdapter)
/** * Created by ruancw on 2018/5/31. * */ public class FilterAdapter extends CommonAdapter<String> { private int selection; public FilterAdapter(Context context, int layoutId, List<String> dataList) { super(context,layoutId,dataList); this.selection = -1; } @Override protected void convert(ViewHolder holder, String value, int position) { holder.setText(R.id.tv_des,value); if(position == selection) { holder.setBackgroundRes(R.id.tv_des,R.color.press); }else { holder.setBackgroundRes(R.id.tv_des,R.color.normal); } } /** * 记录选中的position位置 * @param position 上一次点击的位置 */ public void recordSelectPosition(int position) { this.selection = position; } }
注意:在adapter中有if必须要有else与之对应,不然会出现记录条目的背景色错乱问题
3.Button+PopupWindow封装
(1)在FilterPopupWindow中定义设置数据的方法
/** * 设置数据 * @param mParentList 一级菜单数据集合 * @param mChildList 二级菜单数据集合 */ public void setValue(List<String> mParentList, List<List<String>> mChildList) { this.mParentList = mParentList; this.mChildList = mChildList; //初始化popupWindow的布局文件 init(); }init方法即是开始初始化popupWindow。
(2)定义数据监听接口,用于筛选完成重新更新UI
/** * 监听数据的接口 */ public interface PopupButtonMonitor { //设置回调的方法 void setFilterResult(String filterResult); } /** * 接口绑定 * @param popupButtonMonitor 接口 */ public void setPopupButtonMonitor(PopupButtonMonitor popupButtonMonitor) { this.popupButtonMonitor = popupButtonMonitor; }(3)实现PopupWindow.OnDismissListener的方法
@Override public void onDismiss() { //在popupWindow消失时,将状态设置为正常状态 setNormalStatus(); }
4.使用
(1)布局中
<com.rcw.filterpopup.FilterPopupButton android:id="@+id/filter_popup_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="测试1" android:textSize="20dp" popupbtn:normalBg="@drawable/tab_bkg_line" popupbtn:normalIcon="@drawable/arrow_down" popupbtn:pressBg="@drawable/tab_bkg_selected" popupbtn:pressIcon="@drawable/arrow_up_blue" />使用popupbtn:xx属性需在根布局添加自定义属性声明:
xmlns:popupbtn="http://schemas.android.com/apk/res-auto"
(2)代码中
filterPopup=findViewById(R.id.filter_popup_button); filterPopup.setValue(pList,cList); filterPopup.setPopupButtonMonitor(this);
5.问题
细心的你可能发现,在文中我使用的是FixPopupWindow,没错,这是自定义的popupWindow。为什么要重新定义poupWindow而不使用系统的PopupWindow呢???
写完代码,迫不及待的进行了一番测试,一顿操作猛如虎,结果发现popupWindow的位置是在系统顶部而不是控件的下方,赶紧检查了下代码,发现使用的是showAsDropDown(View anchor)方法,经过一番努力,在stackoverflow中找到原因,原来是一系统级bug,在版本>=24时要在showASDropDown方法重新测量。
/** * Created by ruancw on 2018/5/30. * 重新定义popupWindow,解决版本过高,popupWindow的位置不在控件下方的问题 */ public class FixPopupWindow extends PopupWindow { public FixPopupWindow(View contentView, int width, int height) { super(contentView, width, height); } @Override public void showAsDropDown(View anchor, int xoff, int yoff) { if(Build.VERSION.SDK_INT >= 24) { Rect rect = new Rect(); anchor.getGlobalVisibleRect(rect); int h = anchor.getResources().getDisplayMetrics().heightPixels - rect.bottom; setHeight(h); } super.showAsDropDown(anchor, xoff, yoff); } }
鸿洋大神recyclerView的adapter依赖:
implementation 'com.zhy:base-rvadapter:3.0.3'
至此,自定义带popupWindow的二级菜单筛选控件简单的封装就实现了,不足之处,欢迎评论与留言!!!