Android自定义PopupWindow实现流式布局筛选控件(一)

前言:因公司项目重构需要,添加了二级菜单筛选及类似商品分类筛选的功能。上一篇文章介绍了带二级菜单的筛选控件,今天介绍类似流式布局的筛选控件,该控件继承自PopupWindow,并解决了高版本的显示问题。本篇文章的控件只能实现单选效果,《Android实现类似京东筛选的流式布局标签(可单选/多选)》通过自定义GridLayout实现可设置单选/多选的流式布局筛选效果,有兴趣的可以移步到此文章查看。

先上效果图:

效果图

实现方式:

1.继承自PopupWindow

2.linearLayout+GridLayout显示数据

3.接口回调,更新UI

1.定义PopupWindow内部类Builder

注:Builder类用于设置PopupWindow的参数设置、popupWindow布局文件初始化、GridLayout布局的数据展示等

(1)定义参数设置方法

private Context context;//上下文对象 
private List<FilterModel> listData;//要显示的数据集合 
private int columnCount;//列数 
private GridLayout mGridLayout;//用于显示流式布局 
private LinearLayout llContent;//popupWindow的内容显示 
//背景颜色 
private int colorBg = Color.parseColor("#F8F8F8"); 
//默认的标题和标签的大小(sp) 
private int titleTextSize = 16; 
private int tabTextSize = 16; 
//标题字体颜色 
private int titleTextColor = Color.parseColor("#333333"); 
//tab标签字体颜色 
private int labelTextColor = R.color.color_popup; 
//tab标签背景颜色 
private int labelBg = R.drawable.shape_circle_bg; 
//当前加载的行数 
private int row = -1; 
private FlowPopupWindow mFlowPopupWindow; 
private List<String> labelLists=new ArrayList<>();
//保存选中的标签数据   
public Builder(Context context) {
    this.context = context; 
}

/**  
* 设置数据集合 * 
*/ 
public void setValues(List<FilterBean> listData) {
    this.listData = listData; 
}

/**  
* 设置gridLayout的列数 
* @param columnCount 列数  
*/ 
public void setColumnCount(int columnCount){
    this.columnCount = columnCount; 
}

/**  
* 设置内容区域的背景色 
* @param color 颜色  
*/ 
public void setBgColor(int color){
    colorBg = context.getResources().getColor(color); 
}

/**  
* 标题字体大小 
* @param titleTextSize 字体大小  
*/ 
public void setTitleSize(int titleTextSize) {
    this.titleTextSize = titleTextSize; 
}

/**  
* tab标签字体大小 
* @param tabTextSize 标签字体大小  
*/ 
public void setLabelSize(int tabTextSize) {
    this.tabTextSize = tabTextSize; 
}

/**  
* 标题字体颜色 
* @param titleTextColor 颜色  
*/ 
public void setTitleColor(int titleTextColor) {
    this.titleTextColor = titleTextColor; 
}

/**  
* tab标签字体颜色 
* @param labelTextColor 颜色  
*/ 
public void setTabColor(int labelTextColor) {
    this.labelTextColor = labelTextColor; 
}

/**  
* 设置标签的背景色 
* @param labelBg 背景色(drawable)  
*/ 
public void setLabelBg(int labelBg) {
    this.labelBg = labelBg; 
}

(2)定义build类,初始化popupWindow布局及GridLayout布局

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void build(){
    //初始化popupWindow的布局文件
    initPopup(getRowCount(),columnCount);
    //设置gridLayout的数据
    setGridData(); 
}

A、初始化PopupWindow布局:initPopup方法

/**  
* 初始化PopupWindow的布局 
* @param rowCount 行数  
* @param columnCount 列数  
*/ 
private void initPopup(int rowCount,int columnCount){
    //初始化popupWindow的布局文件
    View view = LayoutInflater.from(context).inflate(R.layout.flow_popup,null);
    //主要用于设置数据显示区域的背景色
    LinearLayout ll=view.findViewById(R.id.ll);
    //流布局数据展示控件
    mGridLayout=view.findViewById(R.id.grid_layout);
    //确定按钮
    Button btnConfirm=view.findViewById(R.id.btn_confirm);
    //设置数据展示区域的背景色
    ll.setBackgroundColor(colorBg);
    llContent = new LinearLayout(context);
    LinearLayout.LayoutParams params = new       LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
    view.setLayoutParams(params);
    llContent.addView(view);
    //设置背景色,不设置的话在有些机型会不显示popupWindow
    llContent.setBackgroundColor(Color.argb(60, 0, 0, 0));
    llContent.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            hidePopup();
        }
    });
  //确定按钮的点击事件
  btnConfirm.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //监听接口的数据回调方法
            flowPopupMonitor.setFlowPopupResult(labelLists);
            //隐藏popupWindow
            hidePopup();
        }
   });
  //设置mGridLayout的属性参数
  mGridLayout.setOrientation(GridLayout.HORIZONTAL);
  mGridLayout.setRowCount(rowCount);
  mGridLayout.setColumnCount(columnCount);
  //设置gridLayout消费触摸事件
  mGridLayout.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return true;
        }
  });
  int padding = context.getResources().getDimensionPixelSize(R.dimen.dp_10);
  mGridLayout.setPadding(padding,padding,padding,padding); 
}

隐藏popupWindow方法:hidePopup()

/**  
* 隐藏popupWindow 
*/ 
private void hidePopup() {
    if (mFlowPopupWindow != null&&mFlowPopupWindow.isShowing()){
        mFlowPopupWindow.dismiss();
    }
}

B、设置GridLayout的数据展示

/**  
* 将数据设置给GridLayout 
*/ 
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void setGridData() {
    for (int i = 0; i < listData.size(); i++){
        //行数++
        ++row;
        //显示每个条目类型的控件
        TextView tvType = new TextView(context);
        tvType.setText(listData.get(i).getTypeName());
        tvType.setTextColor(titleTextColor);
        tvType.setTextSize(titleTextSize);
        //配置列 第一个参数是起始列标 第二个参数是占几列 title(筛选类型)应该占满整行,so -> 总列数
        GridLayout.Spec columnSpec = GridLayout.spec(0,columnCount);
        //配置行 第一个参数是起始行标  起始行+起始列就是一个确定的位置
        GridLayout.Spec rowSpec = GridLayout.spec(row);
        //将Spec传入GridLayout.LayoutParams并设置宽高为0或者WRAP_CONTENT,必须设置宽高,否则视图异常
        GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
        layoutParams.width = GridLayout.LayoutParams.WRAP_CONTENT;
        layoutParams.height = GridLayout.LayoutParams.WRAP_CONTENT;
        layoutParams.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
        layoutParams.bottomMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_8);
        mGridLayout.addView(tvType,layoutParams);
        //添加tab标签
        addTabs(listData.get(i),i);
    }
}

添加tab标签的方法

/**  
* 添加tab标签 
* @param model 数据bean  
* @param labelIndex 标签的标号  
*/ 
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void addTabs(final FilterBean bean, final int labelIndex){
    List<FilterBean.TableMode> tabs = bean.getTabs();
    for (int i = 0; i < tabs.size(); i++){
        if (i % columnCount == 0){
            row ++;
        }
        final FilterBean.TableMode tab = tabs.get(i);
        //显示标签的控件
        final TextView label = new TextView(context);
        //设置默认选中第一个
        if (i==0) {
            //每个tab的第一个设置为选中
            label.setSelected(true);
            //记录选中的tab值
            bean.setTab(tab);
        }
        label.setTextSize(tabTextSize);
        label.setTextColor(context.getResources().getColorStateList(labelTextColor));
        label.setBackgroundDrawable(context.getResources().getDrawable(labelBg));
        label.setSingleLine(true);
        label.setGravity(Gravity.CENTER);
        label.setEllipsize(TextUtils.TruncateAt.MIDDLE);
        //上下padding值
        int paddingT = context.getResources().getDimensionPixelSize(R.dimen.dp_5);
        //左右padding值
        int paddingL = context.getResources().getDimensionPixelSize(R.dimen.dp_8);
        label.setPadding(paddingL,paddingT,paddingL,paddingT);
        //getItemLayoutParams用于设置label标签的参数
        mGridLayout.addView(label,getItemLayoutParams(i,row));
        label.setText(tab.name);
       if (tabs.get(i) == bean.getTab()){
            label.setSelected(true);
       }
      //标签的点击事件
      label.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (tab != bean.getTab()){
                    Log.e("rcw","index--->"+getIndex(bean,labelIndex));
                    //清空上次选中的状态
                    mGridLayout.getChildAt(getIndex(bean,labelIndex)).setSelected(false);
                    //设置当前点击选中的tab
                    bean.setTab(tab);</pre>
                    label.setSelected(true); 
                    String labelText=label.getText().toString(); 
                    labelLists.add(bean.getTypeName()+"-"+labelText);Log.e("rcw","labelText--->"+bean.getTypeName()+"-"+labelText); 
                } 
             } 
       }); 
   }
}

设置GridLayout的item的属性参数的方法

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private GridLayout.LayoutParams getItemLayoutParams(int i, int row){
    //使用Spec定义子控件的位置和比重
    GridLayout.Spec rowSpec = GridLayout.spec(row,1f);
    GridLayout.Spec columnSpec = GridLayout.spec(i%columnCount,1f);
    //将Spec传入GridLayout.LayoutParams并设置宽高为0,必须设置宽高,否则视图异常
    GridLayout.LayoutParams lp = new GridLayout.LayoutParams(rowSpec, columnSpec);
    lp.width = 0;
    lp.height = GridLayout.LayoutParams.WRAP_CONTENT;
    lp.bottomMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_8);
    if(i % columnCount == 0) {//最左边
       lp.leftMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_10);
       lp.rightMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_20);
    }else if((i + 1) % columnCount == 0){//最右边
       lp.rightMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_10);
    }else {//中间
       lp.rightMargin = context.getResources().getDimensionPixelSize(R.dimen.dp_20);
    }
    return lp; 
}

其他相关方法

/**  
* 获取当前选中标签在整个GridLayout的索引 
* @return 标签下标  
*/ 
private int getIndex(FilterBean bean, int labelIndex){
    int index = 0;
   for (int i = 0; i < labelIndex; i++){
        //计算当前类型之前的元素所占的个数 title算一个
        index += listData.get(i).getTabs().size() + 1;
  }
    //加上当前 title下的索引
    FilterModel.TableMode tableModel = bean.getTab();
    index += bean.getTabs().indexOf(tableModel) + 1;
     return index; 
}

/**  
* 获取内容行数 
* @return 行数  
*/ 
private int getRowCount(){
    int row = 0;
    for (FilterBean bean : listData){
        //计算当前类型之前的元素所占的个数 标题栏也算一行
        row ++;
        int size = bean.getTabs().size();
        row += (size / columnCount) + (size % columnCount > 0 ? 1 : 0) ;
    }
    return row; 
}

(3)定义创建PopupWindow的方法

/**  
* 创建popupWindow 
* @return FlowPopupWindow实例  
*/ 
public FlowPopupWindow createPopup(){
    if (listData == null || listData.size() == 0){
        try {
            throw new Exception("没有筛选标签");
        } catch (Exception e) {
            Toast.makeText(context,e.getMessage(),Toast.LENGTH_SHORT).show();
            e.printStackTrace();
    }
        return null;
  }
    mFlowPopupWindow = new FlowPopupWindow(context,llContent);
    return mFlowPopupWindow; 
}

注:以上定义的方法均是在内部类Builder中实现的

2.重写构造方法及showAsDropDown

(1)构造方法

private FlowPopupWindow(Context context,View view){
    //这里可以修改popupWindow的宽高
    super(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    setContentView(view);
    //设置popupWindow弹出和消失的动画效果
    //setAnimationStyle(R.style.popwin_anim_style); //设置有焦点  setFocusable(true);
    //设置点击外部可消失
    setOutsideTouchable(true); 
}

(2)showAsDropDown方法

重写showAsDropDown方法的目的是为了解决高版本不兼容的问题,在高版本中,popupWindow的位置不会出现在相应控件的下方,而是在系统状态栏的地方,有兴趣的可以注掉重写的showAsDropDown方法在高版本手机中进行测试。

/**  
* 重写showAsDropDown方法,解决高版本不在控件下方显示的问题 
* @param anchor popupWindow要显示在的控件  
*/ 
@Override public void showAsDropDown(View anchor) {
    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); 
}

3.定义接口回调方法

private static FlowPopupMonitor flowPopupMonitor;   
public interface FlowPopupMonitor{
    void setFlowPopupResult(List<String> filterResult); 
}

public void setFlowPopupMonitor(FlowPopupMonitor flowPopupMonitor){
    this.flowPopupMonitor=flowPopupMonitor; 
}

注:FlowPopupMonitor接口的实现方法setFlowPopupResult是在确定按钮点击事件中调用的。

4.控件使用

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void initFlowPopup() {
    FlowPopupWindow.Builder builder=new FlowPopupWindow.Builder(context);
    //设置数据
    builder.setValues(lists);
    //设置标签字体的颜色,这里的color不是values目录下的color,而是res文件夹下的color
    builder.setLabelColor(R.color.color_popup);
    //设置标签的背景色
    builder.setLabelBg(R.drawable.flow_popup);
    //设置GridLayout的列数
    builder.setColumnCount(4);
    //初始化popupWindow的相关布局及数据展示
    builder.build();
    //创建popup
    mFixPopupWindow=builder.createPopup();
    //设置数据监听接口
    mFixPopupWindow.setFlowPopupMonitor(this);
    mFixPopupWindow.showAsDropDown(btn2);
    mFixPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
        @Override
        public void onDismiss() {
            ivArrow.setImageResource(R.drawable.arrow_down);
        }
    }); 
}

注:setLabelColor中的color不是values目录下的color,是在res文件夹下color

附上数据类的Bean:

/**  
* Created by ruancw on 2018/5/31. 
* 用于筛选的数据类 
*/   
public class FilterBean {
    private String typeName;//标题名字
    private TableMode tab;//用于记录上次点击的位置
    private List<TableMode> tabs; //标签集合    
    public FilterBean(String typeName, TableMode tab, List<TableMode> tabs) {
        this.typeName = typeName;
        this.tab = tab;
        this.tabs = tabs;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    public TableMode getTab() {
        return tab;
    }

    public void setTab(TableMode tab) {
        this.tab = tab;
    }

    public List<TableMode> getTabs() {
        return tabs;
    }

    public void setTabs(List<TableMode> tabs) {
        this.tabs = tabs;
    }

    public static class TableMode{
        String name;   
        public TableMode(String name) {
            this.name = name;
        }
    }
}

总结:通过自定义PopupWindow的方式,使用LinearLayout+GridLayout的布局,实现了类似流布局的筛选控件,并通过重写showAsDropDown方法解决高版本显示的问题。

注:Android自定义PopupWindow实现流式布局筛选控件(二)对本篇文章代码做了部分修改,修复了返回数据的bug,有兴趣的请移步链接地址文章。

如有任何疑问,欢迎评论留言,谢谢!!!

Android自定义带popupWindow的二级菜单筛选控件:
https://blog.csdn.net/ruancw/article/details/80522881

参考链接:https://www.2cto.com/kf/201804/735958.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容