FlowLayout详解(二)——FlowLayout实现

经过上篇的铺垫,这篇就开始正式开始FlowLayout的开发啦,还是先给大家上上效果:


从效果图中可以看到,底部container的布局方式应该是layout_width="match_parent",layout_height="wrap_content";
好了,废话不多说了,下面开始进入正规。

一、XML布局

从布局图中可以看到,FlowLayout中包含了很多TextView.难度不大,布局代码如下:
先定义一个style,这是为FlowLayout中的TextView定义的:

[html]  view plain  copy
  1. <style name="text_flag_01">  
  2.     <item name="android:layout_width">wrap_content</item>  
  3.     <item name="android:layout_height">wrap_content</item>  
  4.     <item name="android:layout_margin">4dp</item>  
  5.     <item name="android:background">@drawable/flag_01</item>  
  6.     <item name="android:textColor">#ffffff</item>  
  7. </style>  
注意,注意!!!我们这里定义了margin,还记得上篇中怎么样提取Margin值吗?重写generateLayoutParams()函数。
下面看activity_main.xml的布局代码:
[html]  view plain  copy
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context=".MainActivity">  
  6.   
  7.     <com.example.harvic.myapplication.FlowLayout  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:background="#ff00ff">  
  11.   
  12.         <TextView  
  13.             style="@style/text_flag_01"  
  14.             android:background="@drawable/flag_03"  
  15.             android:text="Welcome"  
  16.             android:textColor="#43BBE7" />  
  17.   
  18.         <TextView  
  19.             style="@style/text_flag_01"  
  20.             android:background="@drawable/flag_03"  
  21.             android:text="IT工程师"  
  22.             android:textColor="#43BBE7" />  
  23.   
  24.         <TextView  
  25.             style="@style/text_flag_01"  
  26.             android:background="@drawable/flag_03"  
  27.             android:text="我真是可以的"  
  28.             android:textColor="#43BBE7" />  
  29.   
  30.         <TextView  
  31.             style="@style/text_flag_01"  
  32.             android:background="@drawable/flag_03"  
  33.             android:text="你觉得呢"  
  34.             android:textColor="#43BBE7" />  
  35.   
  36.         <TextView  
  37.             style="@style/text_flag_01"  
  38.             android:background="@drawable/flag_03"  
  39.             android:text="不要只知道挣钱"  
  40.             android:textColor="#43BBE7" />  
  41.   
  42.         <TextView  
  43.             style="@style/text_flag_01"  
  44.             android:background="@drawable/flag_03"  
  45.             android:text="努力ing"  
  46.             android:textColor="#43BBE7" />  
  47.   
  48.         <TextView  
  49.             style="@style/text_flag_01"  
  50.             android:background="@drawable/flag_03"  
  51.             android:text="I thick i can"  
  52.             android:textColor="#43BBE7" />  
  53.   
  54.         </com.example.harvic.myapplication.FlowLayout>  
  55. </LinearLayout>  
这里注意两点,FlowLayout的android:layout_width设置为"match_parent",android:layout_height设置为""wrap_content";同时,我们为FlowLayout添加背景来明显看出我们计算出来的所占区域大小。

二、提取margin与onMeasure()重写

1、提取margin

上篇我们讲过要提取margin,就一定要重写generateLayoutParams

[java]  view plain  copy
  1. @Override  
  2. protected LayoutParams generateLayoutParams(LayoutParams p)  
  3. {  
  4.     return new MarginLayoutParams(p);  
  5. }  
  6.   
  7. @Override  
  8. public LayoutParams generateLayoutParams(AttributeSet attrs)  
  9. {  
  10.     return new MarginLayoutParams(getContext(), attrs);  
  11. }  
  12.   
  13. @Override  
  14. protected LayoutParams generateDefaultLayoutParams()  
  15. {  
  16.     return new MarginLayoutParams(LayoutParams.MATCH_PARENT,  
  17.             LayoutParams.MATCH_PARENT);  
  18. }  
具体为什么我们就不再讲了,上篇已经讲的非常熟悉了,下面就看看如何在onMeasure()中计算当前container所占的位置大小。

2、重写onMeasure()——计算当前FlowLayout所占的宽高

这里就要重写onMeasure()函数,在其中计算所有当前container所占的大小。
要做FlowLayout,首先涉及下面几个问题:
(1)何时换行
从效果图中可以看到,FlowLayout的布局是一行行的,如果当前行已经放不下下一个控件,那就把这个控件移到下一行显示。所以我们要有个变量来计算当前行已经占据的宽度,以判断剩下的空间是否还能容得下下一个控件。
(2)、如何得到FlowLayout的宽度
FlowLayout的宽度是所有行宽度的最大值,所以我们要记录下每一行的所占据的宽度值,进而找到所有值中的最大值。
(3)、如何得到FlowLayout的高度
很显然,FlowLayout的高度是每一行高度的总和,而每一行的高度则是取该行中所有控件高度的最大值。
原理到这里就讲完了,下面看看代码:

(1)首先,刚进来的时候是利用MeasureSpec获取系统建议的数值的模式

[java]  view plain  copy
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  3.     int measureWidth = MeasureSpec.getSize(widthMeasureSpec);  
  4.     int measureHeight = MeasureSpec.getSize(heightMeasureSpec);  
  5.     int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);  
  6.     int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);  
  7.     ………………  
  8. }  
(2)然后,是计算FlowLayout所占用的空间大小
先申请几个变量:
[java]  view plain  copy
  1. int lineWidth = 0;//记录每一行的宽度  
  2. int lineHeight = 0;//记录每一行的高度  
  3. int height = 0;//记录整个FlowLayout所占高度  
  4. int width = 0;//记录整个FlowLayout所占宽度  
然后开始计算:(先贴出代码,再细讲)
[java]  view plain  copy
  1. int count = getChildCount();  
  2. for (int i=0;i<count;i++){  
  3.     View child = getChildAt(i);  
  4.     measureChild(child,widthMeasureSpec,heightMeasureSpec);  
  5.       
  6.     MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  7.     int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;  
  8.     int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;  
  9.   
  10.     if (lineWidth + childWidth > measureWidth){  
  11.         //需要换行  
  12.         width = Math.max(lineWidth,childWidth);  
  13.         height += lineHeight;  
  14.         //因为由于盛不下当前控件,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth  
  15.         lineHeight = childHeight;  
  16.         lineWidth = childWidth;  
  17.     }else{  
  18.         // 否则累加值lineWidth,lineHeight取最大高度  
  19.         lineHeight = Math.max(lineHeight,childHeight);  
  20.         lineWidth += childWidth;  
  21.     }  
  22.   
  23.     //最后一行是不会超出width范围的,所以要单独处理  
  24.     if (i == count -1){  
  25.         height += lineHeight;  
  26.         width = Math.max(width,lineWidth);  
  27.     }  
  28.   
  29. }  
在整个For循环遍历每个控件时,先计算每个子控件的宽和高,代码如下:
[java]  view plain  copy
  1. View child = getChildAt(i);  
  2. measureChild(child,widthMeasureSpec,heightMeasureSpec);  
  3.   
  4. MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  5. int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;  
  6. int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;  
注意我们在计算控件高度和宽度时,要加上上、下、左、右的margin值。
这里一定要注意的是:在调用child.getMeasuredWidth()、child.getMeasuredHeight()之前,一定要调用measureChild(child,widthMeasureSpec,heightMeasureSpec);!!!!在上篇中我们讲过,在onMeasure()之后才能调用getMeasuredWidth()获得值;同样,只有调用onLayout()后,getWidth()才能获取值。

下面就是判断当前控件是否换行及计算出最大高度和宽度了:

[java]  view plain  copy
  1. if (lineWidth + childWidth > measureWidth){  
  2.      //需要换行  
  3.      width = Math.max(lineWidth,width);  
  4.      height += lineHeight;  
  5.      //因为由于盛不下当前控件,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth  
  6.      lineHeight = childHeight;  
  7.      lineWidth = childWidth;  
  8.  }else{  
  9.      // 否则累加值lineWidth,lineHeight取最大高度  
  10.      lineHeight = Math.max(lineHeight,childHeight);  
  11.      lineWidth += childWidth;  
  12.  }  
由于lineWidth是用来累加当前行的总宽度的,所以当lineWidth + childWidth > measureWidth时就表示已经容不下当前这个控件了,这个控件就需要转到下一行;我们先看else部分,即不换行时怎么办?
在不换行时,计算出当前行的最大高度,同时将当前子控件的宽度累加到lineWidth上:
[java]  view plain  copy
  1. lineHeight = Math.max(lineHeight,childHeight);  
  2. lineWidth += childWidth;  
当需要换行时,首先将当前行宽lineWidth与目前的最大行宽width比较计算出最新的最大行宽width,作为当前FlowLayout所占的宽度,还要将行高lineHeight累加到height变量上,以便计算出FlowLayout所占的总高度。
[java]  view plain  copy
  1. width = Math.max(lineWidth,width);  
  2. height += lineHeight;  
下面就是重新初始化lineWidth和lineHeight了,由于换行,那当前控件就是下一行控件的第一个控件,那么当前行的行高就是这个控件的高,当前行的行宽就是这个控件的宽度值了:
[java]  view plain  copy
  1. lineHeight = childHeight;  
  2. lineWidth = childWidth;  
非常需要注意的是,当计算最后一行时,由于肯定是不会超过行宽的,而我们在for循环中,当不超过行宽中只做了下面处理:
[java]  view plain  copy
  1. //上面if语句的else部分  
  2.  }else{  
  3.      // 否则累加值lineWidth,lineHeight取最大高度  
  4.      lineHeight = Math.max(lineHeight,childHeight);  
  5.      lineWidth += childWidth;  
  6.  }  
在这里,我们只计算了行宽和行高,但并没有将其width和height做计算,所以,当是最后一行的最后一个控件时,我们要单独运算width、height:
[java]  view plain  copy
  1.  //最后一行是不会超出width范围的,所以要单独处理  
  2. if (i == count -1){  
  3.     height += lineHeight;  
  4.     width = Math.max(width,lineWidth);  
  5. }  
(3)最后,通过setMeasuredDimension()设置到系统中:
[java]  view plain  copy
  1. setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth  
  2.         : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight  
  3.         : height);  
完整的代码如下:
[java]  view plain  copy
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.    super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  3.    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);  
  4.    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);  
  5.    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);  
  6.    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);  
  7.   
  8.   
  9.    int lineWidth = 0;  
  10.    int lineHeight = 0;  
  11.    int height = 0;  
  12.    int width = 0;  
  13.    int count = getChildCount();  
  14.    for (int i=0;i<count;i++){  
  15.        View child = getChildAt(i);  
  16.        measureChild(child,widthMeasureSpec,heightMeasureSpec);  
  17.          
  18.        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  19.        int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;  
  20.        int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;  
  21.   
  22.        if (lineWidth + childWidth > measureWidth){  
  23.            //需要换行  
  24.            width = Math.max(lineWidth,width);  
  25.            height += lineHeight;  
  26.            //因为由于盛不下当前控件,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth  
  27.            lineHeight = childHeight;  
  28.            lineWidth = childWidth;  
  29.        }else{  
  30.            // 否则累加值lineWidth,lineHeight取最大高度  
  31.            lineHeight = Math.max(lineHeight,childHeight);  
  32.            lineWidth += childWidth;  
  33.        }  
  34.   
  35.        //最后一行是不会超出width范围的,所以要单独处理  
  36.        if (i == count -1){  
  37.            height += lineHeight;  
  38.            width = Math.max(width,lineWidth);  
  39.        }  
  40.   
  41.    }  
  42.    //当属性是MeasureSpec.EXACTLY时,那么它的高度就是确定的,  
  43.    // 只有当是wrap_content时,根据内部控件的大小来确定它的大小时,大小是不确定的,属性是AT_MOST,此时,就需要我们自己计算它的应当的大小,并设置进去  
  44.    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth  
  45.            : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight  
  46.            : height);  
  47. }  

3、重写onLayout()——布局所有子控件

在onLayout()中就是一个个布局子控件了,由于控件要后移和换行,所以我们要标记当前控件的left坐标和top坐标,所以我们要先申请下面几个变量:

[java]  view plain  copy
  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  2.     int count = getChildCount();  
  3.     int lineWidth = 0;//累加当前行的行宽  
  4.     int lineHeight = 0;//当前行的行高  
  5.     int top=0,left=0;//当前坐标的top坐标和left坐标  
  6.     ………………  
  7. }  
然后就是计算每个控件的top坐标和left坐标,然后调用layout(int left,int top,int right,int bottom)来布局每个子控件,代码如下:(先列出来全部代码,然后再细讲)
[java]  view plain  copy
  1. for (int i=0; i<count;i++){  
  2.     View child = getChildAt(i);  
  3.     MarginLayoutParams lp = (MarginLayoutParams) child  
  4.             .getLayoutParams();  
  5.     int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;  
  6.     int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;  
  7.   
  8.     if (childWidth + lineWidth >getMeasuredWidth()){  
  9.         //如果换行  
  10.         top += lineHeight;  
  11.         left = 0;  
  12.         lineHeight = childHeight;  
  13.         lineWidth = childWidth;  
  14.     }else{  
  15.         lineHeight = Math.max(lineHeight,childHeight);  
  16.         lineWidth += childWidth;  
  17.     }  
  18.     //计算childView的left,top,right,bottom  
  19.     int lc = left + lp.leftMargin;  
  20.     int tc = top + lp.topMargin;  
  21.     int rc =lc + child.getMeasuredWidth();  
  22.     int bc = tc + child.getMeasuredHeight();  
  23.     child.layout(lc, tc, rc, bc);  
  24.     //将left置为下一子控件的起始点  
  25.     left+=childWidth;  
  26. }  
(1)首先,与onMeasure()一样,先计算出当前孩子的宽和高:
[java]  view plain  copy
  1. View child = getChildAt(i);  
  2. MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  3. int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;  
  4. int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;  
(2)然后根据是否要换行来计算当行控件的top坐标和left坐标:
[java]  view plain  copy
  1. if (childWidth + lineWidth >getMeasuredWidth()){  
  2.     //如果换行,当前控件将跑到下一行,从最左边开始,所以left就是0,而top则需要加上上一行的行高,才是这个控件的top点;  
  3.     top += lineHeight;  
  4.     left = 0;  
  5.      //同样,重新初始化lineHeight和lineWidth  
  6.     lineHeight = childHeight;  
  7.     lineWidth = childWidth;  
  8. }else{  
  9.     // 否则累加值lineWidth,lineHeight取最大高度  
  10.     lineHeight = Math.max(lineHeight,childHeight);  
  11.     lineWidth += childWidth;  
  12. }  
在计算好left,top之后,然后分别计算出控件应该布局的上、下、左、右四个点坐标:
需要非常注意的是margin不是padding,margin的距离是不绘制的控件内部的,而是控件间的间隔!
[java]  view plain  copy
  1. int lc = left + lp.leftMargin;//左坐标+左边距是控件的开始位置  
  2. int tc = top + lp.topMargin;//同样,顶坐标加顶边距  
  3. int rc =lc + child.getMeasuredWidth();  
  4. int bc = tc + child.getMeasuredHeight();  
  5. child.layout(lc, tc, rc, bc);  
最后,计算下一坐标的位置:由于在换行时才会变更top坐标,所以在一个控件绘制结束时,只需要变更left坐标即可:
[java]  view plain  copy
  1. //将left置为下一子控件的起始点  
  2. left+=childWidth;  
到这里就结束了,onLayout的完整代码如下:
[java]  view plain  copy
  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  2.    int count = getChildCount();  
  3.    int lineWidth = 0;  
  4.    int lineHeight = 0;  
  5.    int top=0,left=0;  
  6.    for (int i=0; i<count;i++){  
  7.        View child = getChildAt(i);  
  8.        MarginLayoutParams lp = (MarginLayoutParams) child  
  9.                .getLayoutParams();  
  10.        int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;  
  11.        int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;  
  12.   
  13.        if (childWidth + lineWidth >getMeasuredWidth()){  
  14.            //如果换行,当前控件将跑到下一行,从最左边开始,所以left就是0,而top则需要加上上一行的行高,才是这个控件的top点;  
  15.            top += lineHeight;  
  16.            left = 0;  
  17.            //同样,重新初始化lineHeight和lineWidth  
  18.            lineHeight = childHeight;  
  19.            lineWidth = childWidth;  
  20.        }else{  
  21.            lineHeight = Math.max(lineHeight,childHeight);  
  22.            lineWidth += childWidth;  
  23.        }  
  24.        //计算childView的left,top,right,bottom  
  25.        int lc = left + lp.leftMargin;  
  26.        int tc = top + lp.topMargin;  
  27.        int rc =lc + child.getMeasuredWidth();  
  28.        int bc = tc + child.getMeasuredHeight();  
  29.        child.layout(lc, tc, rc, bc);  
  30.        //将left置为下一子控件的起始点  
  31.        left+=childWidth;  
  32.    }  
  33.   
  34. }  
好啦,有关FlowLayout的系列文章到这里就结束了,这里主要涉及到ViewGroup的绘制流程的相关知识。希望大家能掌握。这篇文章感觉有点乱,难度倒是不大,凡是跟代码有关的东东总是很难驾驭,可能还是语文不行啊,大家见量,多看看源码吧,理解了上一篇之后,这篇难度不大。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值