Android流式布局FlowLayout的实现,Android布局的内部机制onMeasure、onLayout

写在前面

最近项目要求在手机界面上展示一系列标签,标签的内容长度不一,当屏幕的可用宽度不够显示下一个标签时另起一行显示。就像下图所示:

病史

按照Android传统的布局方式,根本不能满足,如果你不怕蛋疼,可以根据不同的手机分辨率一一计算每个标签的长宽然后精准绝对布局。所幸的是我以前做过此类的功能需求,用过一个开源的FlowLayout,所以直接拿来主义,套上老牛车跑起来发现每次换行后的第一个标签总是若隐若现。无奈硬着头皮深入到代码内部打探虚实,一入代码深似海啊,楼主的世界观颠覆了,深埋于心中的三年疑惑终于得到了解答。所以就有了楼主的这篇洋洋洒洒、略显枯燥的文章。

FlowLayout是什么货?

public class FlowLayout extends ViewGroup

FlowLayout继承ViewGroup,主要重写了它的两个方法onMeasure和onLayout。


onMeasure方法

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

    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

    int width = 0;
    int height = getPaddingTop() + getPaddingBottom();

    int lineWidth = 0;
    int lineHeight = 0;
    int childCount = getChildCount();
    for(int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        boolean lastChild = i == childCount - 1;
        if(child.getVisibility() == View.GONE) {
            if(lastChild) {
                width = Math.max(width, lineWidth);
                height += lineHeight;
            }
            continue;
        }
        measureChildWithMargins(child, widthMeasureSpec, lineWidth, heightMeasureSpec, height);
        .......
    }
    width += getPaddingLeft() + getPaddingRight();
    setMeasuredDimension(
            (modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width,
            (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height);
}

onLayout方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    float horizontalGravityFactor;
    switch ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
        case Gravity.LEFT:
        default:
            horizontalGravityFactor = 0;
            break;
        case Gravity.CENTER_HORIZONTAL:
            horizontalGravityFactor = .5f;
            break;
        case Gravity.RIGHT:
            horizontalGravityFactor = 1;
            break;
    }

    for(int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if(child.getVisibility() == View.GONE) {
            continue;
        }
        LayoutParams lp = (LayoutParams) child.getLayoutParams();

        int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        int childHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;

        if(lineWidth + childWidth > width) {
            mLineHeights.add(lineHeight);
            mLines.add(lineViews);
            mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());

            linesSum += lineHeight;

            lineHeight = 0;
            lineWidth = 0;
            lineViews = new ArrayList<View>();
        }

        lineWidth += childWidth;
        lineHeight = Math.max(lineHeight, childHeight);
        lineViews.add(child);
    }

    mLineHeights.add(lineHeight);
    mLines.add(lineViews);
    mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());

    linesSum += lineHeight;

    int verticalGravityMargin = 0;
    switch ((mGravity & Gravity.VERTICAL_GRAVITY_MASK)  ) {
        case Gravity.TOP:
        default:
            break;
        case Gravity.CENTER_VERTICAL:
            verticalGravityMargin = (height - linesSum) / 2;
            break;
        case Gravity.BOTTOM:
            verticalGravityMargin = height - linesSum;
            break;
    }
    ......
}

友情提示:不用细看上面的代码,我只截取了部分。当你弄明白了Android布局的内部机制(这里只讲onMeasure、onLayout),Android世界的所有UI布局需求,你都能做到一弹即破。


Android布局的机制解析

可以说重载onMeasure(),onLayout(),onDraw()三个函数构建了自定义View的外观形象。再加上onTouchEvent()等重载视图的行为,可以构建任何我们需要的可感知到的自定义View。

我们知道,不管是自定义View还是系统提供的TextView这些,它们都必须放置在LinearLayout等一些ViewGroup中,因此理论上我们可以很好的理解onMeasure(),onLayout(),onDraw()这三个函数:

  1. View本身大小多少,这由onMeasure()决定;
  2. View在ViewGroup中的位置如何,这由onLayout()决定;
  3. 绘制View,onDraw()定义了如何绘制这个View。

一个布局例子

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:gravity="center"  
    android:paddingBottom="@dimen/activity_vertical_margin"  
    android:paddingLeft="@dimen/activity_horizontal_margin"  
    android:paddingRight="@dimen/activity_horizontal_margin"  
    android:paddingTop="@dimen/activity_vertical_margin" >  

    <LinearLayout  
        android:layout_width="200dp"  
        android:layout_height="wrap_content"  
        android:paddingTop="20dp"  
        android:layout_marginTop="30dp"  
        android:background="@android:color/darker_gray" >  
        <com.witspring.view.MyView   
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:paddingTop="10dp"  
            android:layout_marginTop="15dp"  
            android:background="@android:color/holo_red_light"  
            />  
    </LinearLayout>  

</LinearLayout>

这里写图片描述

MyView的Java类代码

public class MyView extends View {  

    public MyView(Context context, AttributeSet attrs) {  
        super(context, attrs);
    }  

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   
        Log.d("MyView","------", new Throwable());// 查看onMeasure()的调用堆栈 
        int speHeightSize = MeasureSpec.getSize(heightMeasureSpec);  
        int speHeightMode = MeasureSpec.getMode(heightMeasureSpec);  
        Log.d("MyView_Height", "---speHeightSize = " + speSize + "");  
        Log.d("MyView_Height", "---speHeightMode = " + speMode + "");  
        if(speHeightMode == MeasureSpec.AT_MOST){  
            Log.d("MyView_Height", "---AT_MOST---");  
        }  
        if(speHeightMode == MeasureSpec.EXACTLY){  
            Log.d("MyView_Height", "---EXACTLY---");  
        }  
        if(speHeightMode == MeasureSpec.UNSPECIFIED){  
            Log.d("MyView_Height", "---UNSPECIFIED---");  
        }  
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), speSize);  
    } 

} 

堆栈调用详情

widthMeasureSpecD/MyView  ( 3506): java.lang.Throwable
D/MyView  ( 3506):     at com.sean.myview.MyView.onMeasure(MyView.java:18)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.widget.LinearLayout.measureVertical(LinearLayout.java:850)
D/MyView  ( 3506):     at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
D/MyView  ( 3506):     at android.view.View.measure(View.java:15775)
D/MyView  ( 3506):     at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView  ( 3506):     at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
......
D/MyView_Height  ( 3506): ---speHeightSize = 940
D/MyView_Height  ( 3506): ---speHeightMode = -2147483648
D/MyView_Height  ( 3506): ---AT_MOST---

通过onMeasure堆栈的调用顺序我们可以得知:

MyView的可测量的高度是由它的父控件逐步传递过来的,它的父控件每次调用自身onMeasure计算出剩余可用的高度后传递给子控件,就这样逐级传递过来的。这里为什么是可测量的呢?

影响speHeightSize的因素为:父视图的layout_height和paddingTop以及自身的layout_marginTop。但是我们不要忘记有weight时的影响。

speHeightMode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。

  • MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width=”50dip”,或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。

  • MeasureSpec.AT_MOST是最大尺寸,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。

  • MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值