Flow layout FlowLayout supports row number control, single selection, multiple selection, click and other operations

Recently there is such a demand, as shown below

The content search page in development often records search keywords , and the search keywords have different lengths. We display them by using a custom flow layout; the basic requirement of the flow layout is to dynamically add childView and implement automatic line wrapping , which is relatively simple. , Override the onMeasure() method of ViewGroup, traverse and dynamically calculate the width and height of each View, and accumulate the width. When it exceeds the width of the ViewGroup, it will be displayed in a new line. It is responsible for setting the measurement mode and size of the sub-controls, and setting its own according to all sub-controls. Width and height; then override the onLayout() method to specify the position and size of all childViews;

Sometimes we also display user labels . The length of labels varies. Not only can labels be clickable, but multiple labels can also be selected. Can we encapsulate a common flow layout? Of course, I encapsulate a flow layout control that supports row number control, single selection, multiple selection, click and other operations;

1. Expose key parameters to the outside world

First add a few externally exposed variables

  • The variable limitLineCount indicates the number of lines displayed by default, and the variable isLimitLine indicates whether there is a limit on the number of lines, which is dynamically set according to itself;
  • Another parameter, isOverFlow, overflows, because the amount of data returned by the interface is uncertain, it may not exceed the row limit, or it may exceed the row limit. If it exceeds, the click to display all button will be displayed, so this parameter plays this role. ;
  • The variables mOnTagClickListener and mOnTagSelectListener represent the tag click and tag selection event callbacks respectively;
  • The variable mTagCheckMode represents the tag selection mode, click, single selection, multiple selection;
  • The variable isMoreListener indicates whether more buttons need to be displayed, that is, beyond the number of lines to be displayed;
    /**
     * 流布局不支持被选中
     */
    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. Limit the number of lines when measuring

If it exceeds, do not continue to measure altitude

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Get the measurement mode and size set for it by its parent container 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);
    }

As you can see, I have defined the parameter int limitLineCount; used to record the number of lines, the core code

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

3. Limit the placement

 @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);
        }
    }

A variable is still added to record the number of rows. When it exceeds, the child view is not placed.

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

At this point, it is almost the same, and the overall layout display control is completed;

4. Customize Adapter to provide data source

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. Data changes refresh the display status of ViewGroup and new Tag (depending on setting mode, click, single selection, multiple selection)

Reinitialize the selected state, and set the label click event listener;

 /**
     * 重新加载刷新数据
     */
    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. How to notify flow layout overflow

When the data is added, it stands to reason that we should know whether the variable at the beginning is overflowed, so as to dynamically display the click to display more styles. The difficulty is coming, how to judge the view drawing is completed? I checked a wave of view processes and found that there is a method dispatchDraw() that is very suitable

RedispatchDraw method in FlowLayout

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

7. FlowLayout usage example

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

refer to

Flow layout FlowLayout and line limit - Excellent Blog - CSDN Blog

Guess you like

Origin blog.csdn.net/ahou2468/article/details/122914957