自定义View之流式布局FlowLayout

来源:互联网 发布:网络卫星电视在线直播 编辑:程序博客网 时间:2024/05/01 16:16

自定义View之流式布局FlowLayout

在我们往常的app设计中,对于一些搜索关键字的推荐,标签等,往往宽度都是不确定的,且当一行满之后会自动换行,类似下面这样,
这里写图片描述
那么今天我们就来实现这个效果。

首先是原理分析。对于该控件,无非就是我们需要获取到每一个子控件的宽,在显示的时候,当某一行的剩余宽度不足以显示下一个控件时,我们让其显示在下一行,继续提炼,主要就是一下两点。
- 在onMeasure()方法中,测量子控件,将其分类,一行显示多少控件,一行需要多大的高度。以及当前控件的大小。
- 在onLayout()中,对子控件进行布局。

那么,让我们开始吧。

首先看一下使用方式:

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        setData();        /**         * 查找控件         */        mFlowLayout = ((FlowLayout) findViewById(R.id.fl));        /**         * 添加数据         */        mFlowLayout.addData(names);        /**         * 设置点击事件         */        mFlowLayout.setFlowLayoutListener(new FlowLayout.FlowLayoutListener() {            @Override            public void onItemClick(View view, int poition) {                Toast.makeText(getApplicationContext(),names.get(poition),Toast.LENGTH_SHORT).show();            }        });    }

根据我们的提炼的两点,分别实现,我们看一下字段,

 /**     * 所有子View,按行记录     */    private List<List<View>> mAllViews = new ArrayList<List<View>>();    /**     * 记录每一行的最大高度     */    private  List<Integer> mLineHeight = new ArrayList<>();    /**     * 标签点击的回调     */    private FlowLayoutListener mFlowLayoutListener;

总共两个字段,一个按行进行存储的所有View的集合,一个用来存储每一行的高度。

先看onMeasure()

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        mAllViews.clear();        mLineHeight.clear();        // 获取当前父容器给当前控件的大小和模式        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);        //每一行的List        List<View> lineView = new ArrayList<>();        //   Log.e(TAG, sizeWidth + "," + sizeHeight);        // 如果是warp_content情况下,记录宽和高        int width = 0;        int height = 0;        /**         * 记录每一行的宽度,width不断取最大宽度         */        int lineWidth = 0;        /**         * 每一行的高度,累加至height         */        int lineHeight = 0;        /**         * 当前控件的宽度         */        int cCount = getChildCount();        // 遍历每个子元素        for (int i = 0; i < cCount; i++)        {            View child = getChildAt(i);            // 测量每一个child的宽和高            measureChild(child, widthMeasureSpec, heightMeasureSpec);            // 得到child的lp            MarginLayoutParams lp = (MarginLayoutParams) child                    .getLayoutParams();            // 当前子空间实际占据的宽度            int childWidth = child.getMeasuredWidth() + lp.leftMargin                    + lp.rightMargin;            // 当前子空间实际占据的高度            int childHeight = child.getMeasuredHeight() + lp.topMargin                    + lp.bottomMargin;            /**             * 如果加入当前child,则超出最大宽度,则的到目前最大宽度给width,类加height 然后开启新行             */            if (lineWidth + childWidth > sizeWidth)            {                width = Math.max(lineWidth,width);// 取最大的                lineWidth = childWidth; // 重新开启新行,开始记录                //记录View                mAllViews.add(lineView);                lineView = new ArrayList<>();                lineView.add(child);                // 叠加当前高度,                height += lineHeight;                mLineHeight.add(lineHeight);                // 开启记录下一行的高度                lineHeight = childHeight;            } else            // 否则累加值lineWidth,lineHeight取最大高度            {                lineView.add(child);                lineWidth += childWidth;                lineHeight = Math.max(lineHeight, childHeight);            }            // 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较            if (i == cCount - 1)            {                width = Math.max(width, lineWidth);                height += lineHeight;                mAllViews.add(lineView);                mLineHeight.add(lineHeight);            }        }        //设置当前控件的宽高        setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth                : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight                : height);    }

onMeasure中,我们首先清空我们两个字段中的内容,因为,在我之前的自定义View之垂直滑动的ViewPager中,发现onLayout和onMeasure()会多次被调用。所以,这一步必不可少。

其次,我们获取当前控件的大小模式,为什么呢,因为,当控件是固定值时,或者match_parent,我们无需管理其大小,但如果属性为wrap_content,我们需要根据,子控件所占位置的大小进行手动控制。(备注:如果为wrap_content,其获取的宽度仍然为屏幕的宽度,高度是当前可显示的最大大小)。

在for循环中,我们测量子类的大小,并获取到MarginLayoutParams用来获取该控件的上,下,左,右的间距。然后进行判断
- 如果当前控件加上其之前的控件的宽度,大于了屏幕的宽度,则我们将当前宽度和之前记录的每一行的宽度比较,去最大值。同时开启新行,添加当前控件的宽度。将保存一行View的list添加到总集合中,开启新的list,添加当前childView到list中。记录高度,因为高度是叠加的,不需要取最大值。同时记录当前childView的高度。
- 如果控件加上之前的控件的宽度仍然小于屏幕的宽度,则叠加这一行的宽度,添加到集合中,同时高度去最大值。

我们需要记录最后一行的高度,以及将控件添加到对应集合中。因为最后一行肯定不满足大于屏幕宽度的条件,但我们需要把他加入到我们数据集合中。

最后,设置当前的宽高,如果mode是EXACTLY,则表示宽高是确定的,我们无需设置。否则,设置为我们测量的值。

下面看一下onLayout()方法

  @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        // 存储每一行所有的childView        List<View> lineViews = new ArrayList<View>();        int lineHeight = 0;        int left = 0;        int top = 0;        // 得到总行数        int lineNums = mAllViews.size();        for (int i = 0; i < lineNums; i++)        {            // 每一行的所有的views            lineViews = mAllViews.get(i);            // 当前行的最大高度            lineHeight = mLineHeight.get(i);            // 遍历当前行所有的View            for (int j = 0; j < lineViews.size(); j++)            {                View child = lineViews.get(j);                if (child.getVisibility() == View.GONE)                {                    continue;                }                MarginLayoutParams lp = (MarginLayoutParams) child                        .getLayoutParams();                //计算childView的left,top,right,bottom                int lc = left + lp.leftMargin;                int tc = top + lp.topMargin;                int rc =lc + child.getMeasuredWidth();                int bc = tc + child.getMeasuredHeight();                child.layout(lc, tc, rc, bc);                left += child.getMeasuredWidth() + lp.rightMargin                        + lp.leftMargin;            }            left = 0;            top += lineHeight;        }    }

onLayout()方法就比较简单了,根据我们的mAllViews,共有多少行,for循环遍历每一行,利用layout方法遍历即可。

控件的显示已经搞定,下面就是点击事件的处理,如果我们把其交给调用者的话,无疑是一种很糟糕的行为,所以我们定义接口进行回调。

   /**     * 标签点击的回调     */    public interface  FlowLayoutListener{        void onItemClick(View view, int poition);    }
    public void setFlowLayoutListener(FlowLayoutListener flowLayoutListener){        mFlowLayoutListener = flowLayoutListener;        for (int i = 0;i<getChildCount();i++){            getChildAt(i).setOnClickListener(this);        }    }

我们在setFlowLayoutListener中,保存接口对象,同时遍历每一个View设置点击。

 @Override    public void onClick(View v) {        for (int i = 0;i<getChildCount();i++){            if(getChildAt(i)==v){                mFlowLayoutListener.onItemClick(v,i);                break;            }        }    }

在onClick方法中,我们利用遍历的方法,通过比对点击的View和childView是否是同一个对象,如果是,则返回第几个childView。

加入添加数据的方法

    /**     * 添加数据     */    public void addData(List<String> datas){        for(String data: datas){            addTextView(data);        }        requestLayout();    }   /**     * 动态添加布局     * @param str     */    private void addTextView(String str) {        TextView child = new TextView(getContext());        ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.MarginLayoutParams.WRAP_CONTENT, ViewGroup.MarginLayoutParams.WRAP_CONTENT);        params.setMargins(15, 15, 15, 15);        child.setLayoutParams(params);        child.setBackgroundResource(R.drawable.shape_text_border);        child.setText(str);        child.setTextSize(18);        child.setTextColor(Color.BLACK);        this.addView(child);    }

我们传入一个字符串集合调用addData方法。如果对于标签的显示模式需要自定义,可以修改addTextView方法。

最后,我们需要加入一个方法

   @Override    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)    {        return new MarginLayoutParams(getContext(), attrs);    }

因为,我们在onMeasure和onLayout中,利用MarginLayoutParams获取margin,但ViewGroup中,默认的是创建ViewGroup.LayoutParams,我们不实现此方法,则已xml文件的方式添加子控件时,会报出类型转换异常。该方法,在ViewGroup的构造方法中,会调用此方法。

使用方法,在最前面已经说明,当然,我们也可以直接在xml中进行添加子控件。

该项目已上传到github,有意者请移步FlowLayout

1 0
原创粉丝点击