拆轮子系列之一步一步教你写FlowLayout
2016-12-08 22:45 阅读(269)

1.前言

在文章的开头,把代码送上,以方便您对照着学习。 
本章教您如何实现自动换行的布局,其实这种控件在很早以前github就有大神实现了,但是不妨碍我们研究它是如何实现的,这对我们的进步有莫大的好处。先看效果图吧 

相信大家能看出它们的共同特点:控件根据ViewGroup的宽,自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行。

2.实现的总体逻辑

1.新建一个类FlowLayout,继承viewGroup。这个不做解释。
2.重写generateLayoutParams。在代码中有详细的介绍。
3.重写onmeasure方法。重写这个方法的目标是获取Flowout的总体宽度,并用setMeasuredDimension设置。
4.重写onLayout方法。重写这个方法的目标是设置每个控件的具体位置。

好了总体的思路我们就按照这个来,下面我们详细的介绍。

3.具体实现

3.1重写generateLayoutParams方法

 /**
     * 这个方法必须重新,否则报错! 
     * public void setLayoutParams(ViewGroup.LayoutParams params){ 
     * if (params == null) { 
     * throw new NullPointerException("Layout parameters cannot be null"); 
     * } 
     * mLayoutParams = params; requestLayout(); 
     * }
     * 我搜遍整个view源码,请看view源码中的setLayoutParams方法,给mLayoutParams赋值的只有这个方法,
     * 大家仔细看:其实是把viewGroup中的params赋值给mLayoutParams。
     * 所以childView.getLayoutParams()
     * ,其实是获取viewGroup中的LayoutParams,也就是我们在generateLayoutParams方法中返回的值。
     */
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

3.2重写onMeasure方法

/**
     * 请牢记本方法的目标: 1.计算高度,并通过setMeasuredDimension设置(本方法的目标)
     * 注意:宽度不需要计算,用MeasureSpec.getSize(widthMeasureSpec)就行。
     * 我们的逻辑如下:
     * 1.遍历viewgroup中的所有子view 
     * 2.遍历期间,我们会不断累加lineWidth 
     * if(lineWidth > MeasureSpec.getSize(widthMeasureSpec)){ 需要换行; 
     *      endHeigth += 每一行中view最大高度。
     *      lineWidth = 0; 重置为0,
     * }else{ 
     *      lineWidth += childWidth; 不断累加
     * } 
     * 3.setMeasuredDimension设置最终高度。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int endHeigth = 0;// 这个是最后setMeasuredDimension要设置的高度,每次换行都要 endHeigth +=

        int lineWidth = 0;// 临时记录当前宽度,如果(lineWidth > measureWidth)就要换行。
        int lineHeigth = 0;// 我们用这个变量记录每一行的最大高度。

        // 以下拆分widthMeasureSpec 和 heightMeasureSpec;具体的原理这里不做详细介绍
        final int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        final int measureHeigth = MeasureSpec.getSize(heightMeasureSpec);
        final int measureHeigthModel = MeasureSpec.getMode(heightMeasureSpec);

        // 获取viewGroup中子控件的数量。
        int childNum = getChildCount();
        // 循环所有的控件是为了计算出总的高度,这也是我们这个方法的目标
        for (int i = 0; i < childNum; i++) {
            View childView = getChildAt(i);
            // 调用子类的onmeasure方法,这个不需要我们干涉,它具体多大的值,其实你在xml中都已经确定了。
            measureChild(childView, widthMeasureSpec, widthMeasureSpec);

            // 得到child的lp,是为了获取子控件的margin值。注意:一定要重写generateLayoutParams方法,不然这里会报错
            MarginLayoutParams lp = (MarginLayoutParams) childView
                    .getLayoutParams();
            // 当前子空间实际占据的宽度
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            // 当前子空间实际占据的高度
            int childHeight = childView.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            /**
             * 核心代码区。这是为了判断是否要换行并且做一些操作。
             *  如果(lineWidth + childWidth) >  measureWidth 
             * 我们应该换行了,并做如下处理 :
                 1.endHeigth += lineHeigth;
                2.lineHeigth = 0;将最大高度重新设置为0;
                 3.lineWidth = 0;将最大宽度重新设置为0; 否则: lineHeigth 比较获取最大值 lineWidth递增
             * 
             * 核心代码就这么一点,是不是很简单啊。
             */
            if (lineWidth + childWidth > measureWidth) {
                endHeigth += lineHeigth;
                lineHeigth = 0;
                lineWidth = lp.leftMargin;
            } else {
                lineHeigth = Math.max(lineHeigth, childHeight);
                lineWidth += childWidth;
            }
        }
        /**
         * 宽度不变,按照测量的来。我们要要设置高度就行,确保每一个控件都能显示出来。
         *  如果measureHeigthModel == MeasureSpec.EXACTLY ,我们应该以系统给我们的值来确定最终的值 否则就用我的计算的值。
         */
        setMeasuredDimension(measureWidth, (measureHeigthModel == MeasureSpec.EXACTLY) ? measureHeigth : endHeigth);
    }

这里,我们并没有设置宽度,是因为我觉的没有必要。我们只设置了高度。

重写onLayout方法

/**
     * 本方法的目标:摆放每个子控件的位置
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = r - l;// 好像有多种方法,都尝试一下

        /**
         * 我们下面for循环的目的就是给每个view确定坐标(即以下的4个值),
         * 然后通过childView.layout(preLeft, preTop, endBottom, endRight)设置。
         */
        int preLeft = 0; // 这个是记录每个控件最终l 坐标
        int preTop = 0; // 这个是记录每个控件最终的 t 坐标
        int endBottom = 0;// 这个是最终记录每个控件的b坐标
        int endRight = 0;// 这个是最终记录每个控件的r坐标


        // 获取viewGroup中子控件的数量。
        int childNum = getChildCount();
        // 循环所有的控件是为了计算出总的高度,这也是我们这个方法的目标
        for (int i = 0; i < childNum; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() == View.GONE) {
                continue;
            }

            // 得到child的lp,是为了获取子控件的margin值。注意:一定要重写generateLayoutParams方法,不然这里会报错
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            // 当前子空间实际占据的宽度
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;


            /**
             * 以下是逻辑处理核心代码,我这个逻辑调了半天才调出来,人个感觉不太满意(肯定有更简单的算法!)大家可以重新写一个。
             * 逻辑如下:
             * 1.我的逻辑始终是给下面这4个变量赋值,这4个变量标识着当前控件的4个点的坐标
             *           preLeft = 0; // 这个是记录每个控件最终left 坐标
                         preTop = 0; // 这个是记录每个控件最终的 top 坐标
                         endBottom = 0;// 这个是最终记录每个控件的bottom坐标
                         endRight = 0;// 这个是最终记录每个控件的right坐标
                         而
               2.if(endRight + childWidth) > width){//如果横向再加一个控件超出了layout总宽度就换行。
                    处理逻辑步骤如下 :
                    1.preLeft = lp.leftMargin;//首先要初始化preLeft,别忘了还有marginLeft;
                    2.preTop = endBottom + lp.topMargin;//起始坐标应该是 endBottom,别忘了还有下一个控件的MarginTop
                    3.有了left坐标和top坐标就好办多了,我们只需要在它们的基础上分别加上控件的宽度和高度。
                    4.坐标都有了,调用 childView.layout确定控件的位置
                    5.最后别忘了endBottom 和 endRight 加上相应的margin值 

               }

             */

            if((endRight + childWidth) > width){ //如果横向再加一个控件超出了layout总宽度就换行。
                 preLeft = lp.leftMargin;//首先要初始化preLeft,别忘了还有marginLeft;
                 preTop = endBottom + lp.topMargin;//起始坐标应该是 endBottom,别忘了还有下一个控件的MarginTop
                 //有了left坐标和top坐标就好办多了,我们只需要在它们的基础上分别加上控件的宽度和高度,下面2句话就做这个事。
                 endBottom =  preTop + childView.getMeasuredHeight();
                 endRight = preLeft + childView.getMeasuredWidth();
                 childView.layout(preLeft, preTop,endRight , endBottom);//画控件的位置
                //最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一行坐标的正确。
                 endBottom +=  lp.bottomMargin;
                 endRight += lp.rightMargin;
            }else{//else中的代码很明显的特点是所有的控件在一行上,所以preLeft和preTop的值是不变的。
                if(i==0){//之所以要判断0,是因为一开始控件的4个坐标值是0,我们要初始化一下,后面的在else中自动赋值了。
                     //为4个坐标初始化
                     preLeft = lp.leftMargin;
                     preTop = lp.topMargin;
                     endBottom =  preTop + childView.getMeasuredHeight();
                     endRight = preLeft + childView.getMeasuredWidth();
                     //设置控件的位置
                     childView.layout(preLeft, preTop,endRight , endBottom);
                     //最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一行坐标的正确。
                     endBottom +=  lp.bottomMargin;
                     endRight += lp.rightMargin;
                }else{

                     preLeft = endRight + lp.leftMargin;
                     endRight = preLeft + childView.getMeasuredWidth(); 
                     childView.layout(preLeft, preTop,endRight , endBottom);
                    //最后别忘了endBottom 和 endRight 加上相应的margin值 ,为了保持下一个控件坐标的正确。
                     endRight += lp.rightMargin;
                }
            }

        }
    }

其他代码

res/values/styles.xml中:

<style name="text_flag_01">  
   <item name="android:layout_width">wrap_content</item>  
   <item name="android:layout_height">wrap_content</item>  
   <item name="android:layout_margin">4dp</item>  
   <item name="android:background">@drawable/flag_01</item>  
   <item name="android:textColor">#ffffff</item>

res/drawable/flag_01.xml

<?xml version="1.0" encoding="utf-8"?>  
<shape xmlns:android="http://schemas.android.com/apk/res/android" >  
<solid android:color="#7690A5" >  
</solid>   
<corners android:radius="5dp"/>  
<padding  
    android:bottom="2dp"  
    android:left="10dp"  
    android:right="10dp"  
    android:top="2dp" />    
</shape>

布局文件:

<com.czh.weigetstudy.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >

    <TextView  
        style="@style/text_flag_01"  
        android:text="Welcome" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="IT工程师" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="学习ing" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="恋爱ing" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="挣钱ing" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="努力ing" />  

    <TextView  
        style="@style/text_flag_01"  
        android:text="I thick i can" />  

</com.czh.weigetstudy.FlowLayout>


作者:mffandx