Android View原理解析之布局流程(layout)

提示:本文的源码均取自Android 7.0(API 24)

前言

自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的布局流程进行讲解。相关内容如下:

  • Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
  • Android View原理解析之测量流程(measure)
  • Android View原理解析之布局流程(layout)
  • Android View原理解析之绘制流程(draw)

从View的角度看layout流程

在本系列的第一篇文章中讲到整个视图树(ViewTree)的根容器是DecorView,ViewRootImpl通过调用DecorView的layout方法开启布局流程。layout是定义在View中的方法,我们先从View的角度来看看布局过程中发生了什么。

首先来看一下layout方法中的逻辑,关键代码如下:

/**
 * 通过这个方法为View及其所有的子View分配位置
 * 
 * 派生类不应该重写这个方法,而应该重写onLayout方法,
 * 并且应该在重写的onLayout方法中完成对子View的布局
 *
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
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;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // ① 通过setOpticalFrame或setFrame为View设置坐标,并判断位置是否发生改变
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // ② 如果位置发生了改变,就调用onLayout方法完成布局逻辑
    	onLayout(changed, l, t, r, b);

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        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);
            }
        }
    }
    .......
}

layout方法和measure不同,并没有使用final修饰,但注释中也清清楚楚地写着View的派生类不应该重写这个方法,而应该重写onLayout方法,并且应该在重写的onLayout方法中完成对子View的布局逻辑。

可以看到,在代码①的位置先通过setOpticalFramesetFrame方法为View设置left、right、top、bottom坐标,并记录View的位置相比之前是否发生了变化。setOpticalFrame最终也调用了setFrame方法,只是在这之前对传入的四个参数做了一些更改。setFrame中的主要逻辑其实就是将传入的四个参数分别赋值给View的四个坐标,并且计算View当前的宽高,最后判断位置是否发生了改变(只要四个坐标中的任何一个值发生了变化都会返回true)。那就让我们来看看setFrame中发生了什么吧:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
    	// (A) 只要任何一个坐标不同就认为View的位置发生了变化
    	changed = true; 

        int drawn = mPrivateFlags & PFLAG_DRAWN;// Remember our drawn bit

        // (B) 计算旧的宽高和新的宽高
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        // 如果宽高与原来不同就认为View的大小发生了变化
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // 执行重绘流程
        invalidate(sizeChanged);

        // (C) 为坐标赋新的值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        // (D) 通知View的大小发生变化(最终会调用onSizeChanged方法)
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }

        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            invalidateParentCaches();
        }

        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

这个方法中的逻辑还是很清晰的,首先在代码(A)的位置记录View的位置是否发生改变,然后在代码(B)的位置通过旧坐标和传入的新坐标分别计算View的旧宽高和新宽高,如果两者不同就认为View的大小发生了变化(sizeChanged)。紧接着在代码(C)的位置将传入的参数赋值给View的四个坐标,到了这一步View的位置信息就真正发生变化了。最后在代码(D)的位置,如果sizeChanged为true,就调用sizeChange方法。View#onSizeChanged方法将在这里调用,通知View的大小已经发生改变。View#onSizeChanged是一个空方法,子类可以重写这个方法实现自己的逻辑。

执行完上面的步骤后,如果View的位置发生了改变,将在layout代码②的位置调用onLayout方法完成对子View的布局逻辑,这个方法的代码如下:

/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 * 
 * 派生类应该重写这个方法,并且完成对子View的布局逻辑
 * 
 * @param changed This is a new size or position for this view
 * @param left Left position, relative to parent
 * @param top Top position, relative to parent
 * @param right Right position, relative to parent
 * @param bottom Bottom position, relative to parent
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

可以看到在View中onLayout是个空方法,因为View自身的布局逻辑已经在setFrame方法中完成了,这里是要完成对子View的布局逻辑。但是对于一个纯粹的View而言,它是没有子View的,所以这里自然什么都不用做。

因此,如果我们通过继承View实现自定义View,理论上是不需要重写layout和onLayout方法的,使用系统默认实现就好了。

从ViewGroup的角度看layout流程

讲完了View中的布局逻辑,现在我们再切换到ViewGroup的角度来看看layout流程中都要做些什么。

首先依旧是layout方法,ViewGroup#layout方法代码如下:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        // 调用View的layout方法
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

虽然ViewGroup重写了layout方法,但是关键逻辑依旧是通过调用View#layout实现的,咱们就不在这里耗费时间了,直接看ViewGroup#onLayout方法:

@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);

当我们兴冲冲地找到onLayout方法,才发现这却是一个抽象方法。静下心来想想,ViewGroup作为布局容器的抽象父类,其实是无法提供一个通用布局逻辑的,这一工作只能交给ViewGroup的具体子类实现。但是作为布局容器,必须要实现对子View的布局逻辑,所以ViewGroup将onLayout标记为抽象方法,保证它的子类一定会实现这个方法。

如果我们通过继承ViewGroup的方式实现自定义View,就必须要实现onLayout方法。常规套路就是循环处理子View,根据希望的布局方式计算每个子View的坐标,然后调用子View的layout方法传入计算好的坐标。如果子View也是一个ViewGroup的话,又会在onLayout方法中继续调用它的子View的layout方法,布局流程就这样从顶级容器逐渐向下传播了。

整体的流程图

上面分别从View和ViewGroup的角度讲解了布局流程,这里再以流程图的形式归纳一下整个layout过程,便于加深记忆:

Android View原理解析之布局流程(layout)_第1张图片

小结

和上一篇文章中的测量流程相比,本文的内容相对简单一点,但仅仅依靠阅读很难形成深刻的记忆。不妨打开AndroidStudio,循着本文的脉络试着一步步探索源码中的逻辑,学习效果可能会更好。

参考资料

https://blog.csdn.net/lfdfhl/article/details/51393131
https://blog.csdn.net/a553181867/article/details/51524527

你可能感兴趣的:(Android进阶)