Andriod 从 0 开始自定义控件之 View 的 layout 过程 (八)

1,944 阅读5分钟

转载请标明出处: blog.csdn.net/airsaid/art…
本文出自:周游的博客

前言

在上一篇文章了,我们学习了 View 三大流程之一的 measure 过程,当 measure 过程完成后,View 的大小就测量好了。接下来就到了 layout 的过程了,layout 的过程就是用于确定 View 的位置。下面通过查看源码,来更深入的了解下 layout 的整个过程。

源码分析

View 的 layout 过程是从 ViewRoot 开始的,ViewRoot 的 performTraversals 方法会在 measure 过程执行完后继续执行 performLayout 方法,在该方法中又执行了 View 的 layout 方法:

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 

该方法接收四个参数,分别是 left、top、right、bottom 四个坐标,代表 View 的四个顶点位置,其中 left、top 的参数为 0,right、bottom 则把测量好的宽、高传入了进来,下面继续看下 layout 方法的具体实现:

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    // 记录 View 的原始位置
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    // 判断 isLayoutModeOptical 方法的返回值, true 则执行 setOpticalFrame 方法,否则执行 setFrame 
    // setOpticalFrame 方法最终也走了 setFrame 方法,所以最终都会执行 setFrame 方法
    // setFrame 方法会判断 View 位置是否发生改变, 如果发生改变, 会将 View 新的 left、top、right、bottom 值赋值给成员变量,并返回一个 boolean 值,表示位置是否改变
    // 当 setFrame 完成后,表示 View 本身的位置已经确定
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    // 如果位置发生改变,执行 onLayout 方法,该方法在 View 中是个空实现,需要继承 ViewGroup 的类实现
    // onLayout 方法的作用是 ViewGroup 确定子 View 的位置
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

layout 方法首先通过调用 setFrame 方法判断 View 的位置是否发生改变,如果发生改变,则将新位置的值 left、top、right、bottom 赋值给 mLeft、mTop、mRight、mBottom 这几个成员变量,这四个值保存着 View 的位置信息,我们可以通过 getLeft、getTop、getRight、getBottom 方法获取到。

所以我们如果想要得到 View 的位置信息,那么必须在 setFrame 方法执行完毕后获取,比如说在 onLayout 方法中获取,因为通过看源码我们已经知道,onLayout 方法是在 setFrame 方法之后执行的。

当 setFrame 方法执行完成后,会返回一个 View 位置是否发生改变的 boolean 值,如果发生改变,那么就会走 onLayout 方法,该方法在 ViewGroup 中调用,用于确定子 View 的位置。

由于具体每种布局的实现效果都不同,所以 onLayout 的默认实现,和上一篇 measure 测量过程中 ViewGroup 的 onMeasure 方法一样,是空实现,具体怎么确定子 View 的位置,由 ViewGroup 的具体实现类去作不同实现。

下面,我们可以自定义一个只能包含一个 View 的布局,来实现下 onLayout 方法,有兴趣的同学也可以自己去看看 LinearLayout 等布局的 onLayout 实现。

实例 (单布局)

代码实现:

public class SingleLayout extends ViewGroup {

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(getChildCount() > 0){
            View childAt = getChildAt(0);
            measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(getChildCount() > 0){
            View childAt = getChildAt(0);
            childAt.layout(0, 0, childAt.getMeasuredWidth(), childAt.getMeasuredHeight());
        }
    }
}

该自定义布局很简单,首先在 onMeasure 方法中判断是否有子 View,如果有,那么只测量第一个 View。接着在 onLayout 中判断是否有子 View,如果有,则调用了第一个子 View 的 layout 方法,分别传入 0, 0, childAt.getMeasuredWidth(), childAt.getMeasuredHeight() 四个参数,这四个参数分别代表了子 View 在 当前自定义布局中的位置,也就是放置在当前自定义布局的左上角。

当子 VIew 在 layout 方法中确定了自己的位置后,如果子 View 是 ViewGroup 那么又会去调用 onLayout 方法去确定它子 View 的位置。这样一层一层传递下去,就完成了整个 View 数的 layout 过程。

下面我们把刚刚写的布局运行下,看下效果:

<com.airsaid.layoutdemo.SingleLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World2!"/>
</com.airsaid.layoutdemo.SingleLayout>

运行结果:
这里写图片描述

可以看到,我们尽管在布局下面放了两个 View,但最终显示的只有一个 View。

getMeasuredWidth 和 getWidth 的区别

getMeasuredWidth / getMeasuredHeight 的值是在 onMeausre 方法结束后可以获取到的,getWidth / getHeight 的值是在 onLayout 方法结束后可以获取到的。

通过查看 View 的 getWidth 、getHeight 源码:

public final int getWidth() {
    return mRight - mLeft;
}
public final int getHeight() {
    return mBottom - mTop;
}

结合 mLeft、mTop、mRight、mBottom 这四个成员变量的赋值过程来看,getWidth 方法的返回值刚好就是 getMeasuredWidth,因为 getMeasuredWidth - 0 不就还是 getMeasuredWidth 吗。

但是在某些特殊情况下,还是会导致两者的返回值不相同,比如说以刚刚自定义的 SingleLayout 的例子看来,我如果将调用子 View 的 layout 方法修改为:

childAt.layout(100, 100, childAt.getMeasuredWidth(), childAt.getMeasuredHeight());

那么最终获取的结果,会发现 getWidth 的值比 getMeasuredWidth 的值少 100px。虽然这样做没有啥意义,但是证明了测量宽高并不一定会等于最终的宽高。

还有一种情况,就是当 View 需要多次测量才能确定自己的测量宽高时,那么在前几次的测量过程中得出的测量宽高有可能并不等于最终的测量宽高,这时获取的测量宽高并不一定与最终宽高相等。

参考