流式布局FlowLayout支持行数控制,单选,多选,点击等操作

最近有这么一个需求,如下图

开发中内容搜索页面经常会记录搜索关键字,搜索关键字长度不一,我们通过会采用自定义流布局的方式展示;流布局的基本需要是动态添加childView并实现自动换行操作,这个操作比较简单,重写ViewGroup的onMeasure()方法,遍历动态计算每个View的宽高,宽度累加,当超过ViewGroup宽度时,则换行显示,负责设置子控件的测量模式和大小,根据所有子控件设置自己的宽和高;然后重写onLayout()方法,完成对所有的childView的位置以及大小的指定;

我们有时候也会显示用户标签,标签长度不一,标签不光可以点击,还可以选中多个标签,那么我们是否可以封装一个常见流布局的呢?当然可以,我封装一个支持行数控制,单选,多选,点击等操作的流布局控件;

1.对外暴露关键参数

首先加了几个对外暴露的变量

  • 变量limitLineCount表示默认显示的行数,变量isLimitLine表示是否有行数限制,这个是根据自身去求动态设置的;
  • 另外一个参数isOverFlow是否溢出,因为接口返回的数据数量是不确定的,可能不会超过行限制,也可能超过行限制,如果超过,则显示点击显示全部按钮,所以这个参数是起到这个作用的;
  • 变量mOnTagClickListener和mOnTagSelectListener分别表示标签点击和标签选中事件回调;
  • 变量mTagCheckMode表示标签选中模式,点击,单选,多选;
  • 变量isMoreListener表示是否需要显示更多按钮,即超出要显示的行数;
    /**
     * 流布局不支持被选中
     */
    public static final int FLOW_TAG_CHECKED_NONE = 0;
    /**
     * 流布局支持单选
     */
    public static final int FLOW_TAG_CHECKED_SINGLE = 1;
    /**
     * 流布局支持多选
     */
    public static final int FLOW_TAG_CHECKED_MULTI = 2;

    /**
     * 监听数据集变化
     */
    AdapterDataSetObserver mDataSetObserver;

    /**
     * 含有数据及显示视图的Adapter
     */
    ListAdapter mAdapter;

    /**
     * 标签点击事件回调
     */
    OnTagClickListener mOnTagClickListener;

    /**
     * 标签被选中事件回调
     */
    OnTagSelectListener mOnTagSelectListener;

    /**
     * 是否有行限制
     */
    boolean isLimitLine = true;

    /**
     * 限制显示的行数
     */
    int limitLineCount = 1;

    /**
     * 是否溢出,即超过要求显示行数
     */
    boolean isOverFlow;

    /**
     * 标签流式布局选中模式,默认是不支持选中的
     */
    private int mTagCheckMode = FLOW_TAG_CHECKED_NONE;

    /**
     * 存储选中的tag
     */
    private SparseBooleanArray mCheckedTagArray = new SparseBooleanArray();

    /**
     * 超过限制显示行数函数回调
     */
    private IsMoreListener isMoreListener;

2.测量时做行数限制

如果超过,则不继续测量高度

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获得它的父容器为它设置的测量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取Padding
        // 获得它的父容器为它设置的测量模式和大小
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        //FlowLayout最终的宽度和高度值
        int resultWidth = 0;
        int resultHeight = 0;

        //测量时每一行的宽度,width不断取最大宽度
        int lineWidth = 0;
        //测量时每一行的高度,加起来就是FlowLayout的高度
        int lineHeight = 0;
        //流布局的行数
        int currLines = 0;
        //流布局子视图的数量
        int childCount = getChildCount();
        //遍历每个子元素
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //测量每一个子view的宽和高
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);

            //获取到测量的宽和高
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //因为子View可能设置margin,这里要加上margin的距离
            MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
            int realChildWidth = childWidth + mlp.leftMargin + mlp.rightMargin;
            int realChildHeight = childHeight + mlp.topMargin + mlp.bottomMargin;

            //如果当前一行的宽度加上要加入的子view的宽度大于父容器给的宽度,就换行
            if ((lineWidth + realChildWidth) > sizeWidth) {
                //限制显示行数
                if(isLimitLine){
                    //达到要求显示的行数+1
                    if(currLines == this.limitLineCount){
                        setOverFlow(true);
                        break;
                    }
                }
                //换行,计算所有行中最大的宽度
                resultWidth = Math.max(lineWidth, realChildWidth);
                //叠加当前高度
                resultHeight += realChildHeight;
                //换行了,lineWidth和lineHeight重新算
                lineWidth = realChildWidth;
                lineHeight = realChildHeight;
                currLines++;
            } else{
                //不换行,直接相加
                lineWidth += realChildWidth;
                //每一行的高度取二者最大值
                lineHeight = Math.max(lineHeight, realChildHeight);
            }

            //遍历到最后一个的时候,肯定走的是不换行,则将当前记录的最大宽度和当前lineWidth做比较
            if (i == childCount - 1) {

                //确认是否溢出,即超过要显示的行数+1
                if(isLimitLine){
                    if(currLines == this.limitLineCount){
                        setOverFlow(true);
                        break;
                    }
                }
                resultWidth = Math.max(lineWidth, resultWidth);
                resultHeight += lineHeight;
                currLines++;
            }
            //判断流布局行数是否超过要显示的行数
            if(currLines>this.limitLineCount){
                //超过默认显示行数的
                setOverFlow(true);
            }
            Log.d("FlowTagLayout", "limitLineCount="+limitLineCount+ " i="+i +"currLines = "+currLines+" realChildWidth="+realChildWidth+
                    "sizeWidth="+sizeWidth);
        }
        setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : resultWidth,
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : resultHeight);
            Log.d("FlowLayout", "resultWidth"+resultWidth+"resultHeight"+resultHeight);
    }

可以看到,我自己定义了参数 int limitLineCount;用来记录行数,核心代码

if ((lineWidth + realChildWidth) > sizeWidth) {
                //限制显示行数
                if(isLimitLine){
                    //达到要求显示的行数+1
                    if(currLines == this.limitLineCount){
                        setOverFlow(true);
                        break;
                    }
                }
}

3.放置位置的时候进行限制

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int flowWidth = getWidth();

        int childLeft = 0;
        int childTop = 0;

        int lineCount = 1;

        //遍历子控件,记录每个子view的位置
        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
            View childView = getChildAt(i);

            //跳过View.GONE的子View
            if (childView.getVisibility() == View.GONE) {
                continue;
            }

            //获取到测量的宽和高
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //因为子View可能设置margin,这里要加上margin的距离
            MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();

            if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > flowWidth) {
                if(isLimitLine) {
                    if(lineCount == this.limitLineCount) {
                        break;
                    }
                }
                //换行处理
                childTop += (mlp.topMargin + childHeight + mlp.bottomMargin);
                childLeft = 0;
                lineCount ++;
            }
            //布局
            int left = childLeft + mlp.leftMargin;
            int top = childTop + mlp.topMargin;
            int right = childLeft + mlp.leftMargin + childWidth;
            int bottom = childTop + mlp.topMargin + childHeight;
            childView.layout(left, top, right, bottom);

            childLeft += (mlp.leftMargin + childWidth + mlp.rightMargin);
        }
    }

依旧是添加了个变量用于记录行数,当超过时,不去放置子view

 if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > flowWidth) {
                if(isLimitLine) {
                    if(lineCount == this.limitLineCount) {
                        break;
                    }
                }
                //换行处理
                childTop += (mlp.topMargin + childHeight + mlp.bottomMargin);
                childLeft = 0;
                lineCount ++;
            }

到这里其实已经差不多了,整体布局显示控制完成了;

4.自定义Adapter提供数据源

public class SearchTagAdapter extends BaseAdapter implements OnInitSelectedPosition {

    private final Context mContext;
    private List<HomeRecommendSubFilterCategory> condListBeans;

    public SearchTagAdapter(Context context) {
        this.mContext = context;
        this.condListBeans = new ArrayList<>();
    }

    public void setCondListBeans(List<HomeRecommendSubFilterCategory> condListBeans){
        this.condListBeans = condListBeans;
        if(this.condListBeans == null){
            this.condListBeans = new ArrayList<>();
        }
    }

    @Override
    public int getCount() {
        return condListBeans==null ? 0 : condListBeans.size();
    }

    @Override
    public Object getItem(int position) {
        return condListBeans==null ? null : condListBeans.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.zsz_good_category_tag_view, null);
        TextView tvZszCategoryItem = view.findViewById(R.id.tv_zsz_category_item);
        tvZszCategoryItem.setText(condListBeans.get(position).label_name);
        if(condListBeans.get(position).isClicked){
            tvZszCategoryItem.setTextColor(mContext.getColor(R.color.zsz_color_E8380D));
        }else{
            tvZszCategoryItem.setTextColor(mContext.getColor(R.color.color_333333));
        }

        return view;
    }



    public void onlyAddAll(List<HomeRecommendSubFilterCategory> sub_filter_category_list) {
        condListBeans.addAll(sub_filter_category_list);
        notifyDataSetChanged();
    }

    public void clearAndAddAll(List<HomeRecommendSubFilterCategory> sub_filter_category_list) {
        condListBeans.clear();
        onlyAddAll(sub_filter_category_list);
    }

    @Override
    public boolean isSelectedPosition(int position) {
        return condListBeans.get(position).isClicked;
    }
}

5.数据变化刷新ViewGroup跟新Tag的显示状态(依赖设置模式,点击,单选,多选)

重新初始化选中状态,同时设置标签点击事件监听;

 /**
     * 重新加载刷新数据
     */
    private void reloadData() {
        //移除所有的旧视图
        removeAllViews();
        boolean isSetted = false;
        for (int i = 0; i < mAdapter.getCount(); i++) {
            final int j = i;
            mCheckedTagArray.put(i, false);
            final View childView = mAdapter.getView(i, null, this);
//            addView(childView,
//              new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));//这个构造方法所然能使用但是编译器会报错
            //重新添加视图
            addView(childView, new MarginLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)));
            //获取Tag的选中状态
            if (mAdapter instanceof OnInitSelectedPosition) {
                boolean isSelected = ((OnInitSelectedPosition) mAdapter).isSelectedPosition(i);
                //判断一下模式
                //单选模式
                if (mTagCheckMode == FLOW_TAG_CHECKED_SINGLE) {
                    //单选只有第一个起作用
                    if (isSelected && !isSetted) {
                        mCheckedTagArray.put(i, true);
                        childView.setSelected(true);
                        isSetted = true;
                    }
                //多选模式
                } else if (mTagCheckMode == FLOW_TAG_CHECKED_MULTI) {
                    if (isSelected) {
                        mCheckedTagArray.put(i, true);
                        childView.setSelected(true);
                    }
                }
            }

            childView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    //点击事件回调
                    if (mTagCheckMode == FLOW_TAG_CHECKED_NONE) {
                        if (mOnTagClickListener != null) {
                            mOnTagClickListener.onItemClick(FlowTagLayout.this, childView, j);
                        }
                    } else if (mTagCheckMode == FLOW_TAG_CHECKED_SINGLE) {
                        //判断状态,取消选中状态
                        if (mCheckedTagArray.get(j)) {
                            mCheckedTagArray.put(j, false);
                            childView.setSelected(false);
                            //单选点选状态回调
                            if (mOnTagSelectListener != null) {
                                mOnTagSelectListener.onItemSelect(FlowTagLayout.this, new ArrayList<Integer>());
                            }
                            return;
                        }

                        for (int k = 0; k < mAdapter.getCount(); k++) {
                            mCheckedTagArray.put(k, false);
                            getChildAt(k).setSelected(false);
                        }
                        mCheckedTagArray.put(j, true);
                        childView.setSelected(true);
                        //单选点选状态回调
                        if (mOnTagSelectListener != null) {
                            mOnTagSelectListener.onItemSelect(FlowTagLayout.this, Arrays.asList(j));
                        }
                    } else if (mTagCheckMode == FLOW_TAG_CHECKED_MULTI) {
                        //设置或取消选中状态
                        if (mCheckedTagArray.get(j)) {
                            mCheckedTagArray.put(j, false);
                            childView.setSelected(false);
                        } else {
                            mCheckedTagArray.put(j, true);
                            childView.setSelected(true);
                        }
                        //回调最终选中的List
                        if (mOnTagSelectListener != null) {
                            List<Integer> list = new ArrayList<Integer>();
                            for (int k = 0; k < mAdapter.getCount(); k++) {
                                if (mCheckedTagArray.get(k)) {
                                    list.add(k);
                                }
                            }
                            mOnTagSelectListener.onItemSelect(FlowTagLayout.this, list);
                        }
                    }
                }
            });
        }
    }

6.如何通知流布局溢出了

当添加完数据之后,按理说我们应该知道开头那个变量判断是否溢出,从而动态显示点击显示更多样式,难点来了,怎么判断view绘制完成呢?查了一波view流程,发现有个方法dispatchDraw()很适合

FlowLayout中重新dispatchDraw方法

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if(isLimitLine) {
        ...这里写一个回调,activity中收到后判断isOverFlow,如果溢出,则显示点击更多样式,否则不显示。
    }
}

7.FlowLayout使用示例

MainActivity 
mZszGoodCategory = findViewById(R.id.zsz_good_category);
        mTvShowAllTags = findViewById(R.id.tv_show_all_tags);
        //设置限制显示行数
        mZszGoodCategory.setLimitLineCount(3);
        mZszGoodCategory.setAdapter(historyAdapter);
        //设置流布局显示模式,单选,多选,点击
        mZszGoodCategory.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI);
        //监听是否溢出需要显示查看更多按钮
        mZszGoodCategory.setIsMoreListener(isOverFlow -> {
            if (isOverFlow) {
                mTvShowAllTags.setVisibility(View.VISIBLE);
            } else {
                mTvShowAllTags.setVisibility(View.GONE);
            }
        });
        List<HomeRecommendSubFilterCategory> list = new ArrayList<>();
        for(int i=0; i<30; i++){
            HomeRecommendSubFilterCategory item = new HomeRecommendSubFilterCategory();
            item.id = String.valueOf(i+1);
            item.label_name = "三国演义"+new Random().nextInt(10000);
            list.add(item);
        }
        historyAdapter.clearAndAddAll(list);
        mTvShowAllTags.setText(getString(R.string.show_all_tags, list.size()));
        //切换显示全部还是显示指定行数内容
        mTvShowAllTags.setOnClickListener(v->{
            mZszGoodCategory.setIsLimitLine(!mZszGoodCategory.isLimitLine());
            int tagsControl = mZszGoodCategory.isLimitLine() ? R.string.show_all_tags : R.string.hide_all_tags;
            mTvShowAllTags.setText(getString(tagsControl, list.size()));
        });
        //获取监听选中标签List
        mZszGoodCategory.setOnTagSelectListener(new OnTagSelectListener() {
            @Override
            public void onItemSelect(FlowTagLayout parent, List<Integer> selectedList) {
                for (HomeRecommendSubFilterCategory condListBean : list) {
                    condListBean.isClicked = false;
                }
                for (int pos : selectedList) {
                    list.get(pos).isClicked = true;
                }
                historyAdapter.notifyDataSetChanged();
            }
        });

Demo参考CustomView / FlowLayout · GitCode

参考

流式布局FlowLayout及行数限制_卓越的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/ahou2468/article/details/122914957