图+源码,读懂View的Layout方法

683 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

读懂 View 三大绘制方法的文章

图+源码,读懂View的MeasureSpec - 掘金 (juejin.cn)

图+源码,读懂View的Measure方法 - 掘金 (juejin.cn)

图+源码,读懂View的Layout方法 - 掘金 (juejin.cn)

图+源码,读懂View的Draw方法 - 掘金 (juejin.cn)

前置知识

  • 有Android开发基础
  • 了解 View 体系
  • 了解 View 的 Measure 方法

前言

上文中,我们讲述了 View 里面的 Measure 方法,Measure 方法是页面绘制的三大方法中最为复杂的一个方法。它的 View 流程和 ViewGroup 的流程不尽相同,前者只需根据不同模式测量自身,而后者测量完自身后还需遍历测量子元素。并且他们在调用获取自身的 MeasureSpec 时候又会根据 DecorView 和普通 View 做不同的要求。

Layout 方法并没有如此复杂,相对来说较为简单。本篇文章就带大家学习 View 绘制三大方法的第二个方法——Layout 方法。

Layout 方法的作用和入口

Layout 一词翻译为:布局、布置。从这个英文翻译可以看出,Layout 方法与位置有关,事实的确如此,Layout 方法用于确定元素的位置所在

那么,Layout 方法的入口在哪里呢?在什么时候由什么方法调用呢?

这个问题在上一篇文章中也有提到,感兴趣的同学可以点击查阅。Layout 方法的入口与 Measure 方法类似,它是由 performLayout() 方法调用的,它的调用链是这种样子:performTraversals() -> performLayout() -> layout() 。我们可以在下方的代码中的注释1和2处看到 performLayout() 方法调用了 layout() 方法。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                           int desiredWindowHeight) {
    ...
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//1
        ...
        if (numViewsRequestingLayout > 0) {
            ...
            if (validLayoutRequesters != null) {
                ...
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//2
                ...
            }

        }
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

Layout 流程

源码分析

下面,我们来看一下 View 中 layout 的源码,为了保留可读性,我把一些不讲解的代码注释掉了,且保留了源码的代码注释,感兴趣的同学可以阅读它的注释,会对它有更深的理解。

layout() 方法中,需要传入的 l t r b,其实分别对应着 left、top、right、bottom。即为从 左、上、右、下,View 相对于父布局的距离。对位置的确定的方法,主要在下面的注释1和2处。

/**
 * Assign a size and position to a view and all of its
 * descendants
 *
 * <p>This is the second phase of the layout mechanism.
 * (The first is measuring). In this phase, each parent calls
 * layout on all of its children to position them.
 * This is typically done using the child measurements
 * that were stored in the measure pass().</p>
 *
 * <p>Derived classes should not override this method.
 * Derived classes with children should override
 * onLayout. In that method, they should
 * call layout on each of their children.</p>
 *
 * @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
 */
@SuppressWarnings({"unchecked"})
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;

    boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);//1

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);//2

        ...
    }
    ...
}

接着我们看一下上面代码中注释1的代码。setFrame() 方法做了什么。我们从下面的代码逻辑以及注释中可以看到,这个方法是设置 View 的四个点的位置,并且会返回告知是否位置与之前有变更。执行完这段代码后,layout 就会执行 onLayout() 方法了,我也在下面给出代码。但是我们发现它是一个空方法,改方法的注释里面写道,我们要使用的时候需要重写这个方法。这是为什么呢?这是因为不同的控件有不同的实现,所以该方法就设定让子类去自行设计了。

/**
 * Assign a size and position to this view.
 *
 * This is called from layout.
 *
 * @param ...
 * @return true if the new size and position are different than the
 *         previous ones
 * {@hide}
 */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ...
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

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

        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        ...
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 *
 * Derived classes with children should override
 * this method and call layout on each of
 * their children.
 * @param ...
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

而由于 ViewGroup 是继承 View 后对 layout() 进行了简单的重写,这里便不再赘述。下面我们继续去看看 ViewGroup 的子类,看看 LinearLayout 是如何实现 onLayout() 方法的。我们可以看到,它是对该方法进行了重写,然后分别对两种不同的列表方向进行 layout 位置确定。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

我们继续查看垂直的 layoutVertical() 方法,我们依旧是做了代码省略。从注释1和注释2处,我们可以看到 childTop 是不断在增大的,其实就是是实现了从上到下排序,后来的元素被排在原本元素的下面,而不会重叠。

注释3处的详细代码也已给出,setChildFrame() 方法其实就是调用子元素的 layout() 方法测量子寻找子元素的位置。这样子设计就可以层层传递,把整个 View 树的位置都寻找出来。

void layoutVertical(int left, int top, int right, int bottom) {
    ...

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);//1
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp =
                (LinearLayout.LayoutParams) child.getLayoutParams();

            ...

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;//2
            }

            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                          childWidth, childHeight);//3
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

//注释3的详细代码
private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

layout 的流程到此就讲完了。这里提一个小问题layout 流程是找出 View 的位置,那么 getWidth() 方法获得的位置是 layout 流程得出的位置吗?

一般来说,是同一个位置。layout 流程的位置是称为最终位置(或最终宽高),而 measure 流程的称为测量位置(或测量宽高) 。两者只是赋值时机不同。阅读这两篇文章后,我们分析流程,你会发现,getWidth()getMeasuredWidth() 两者得到的结果是一样的,我们也可认为测量宽高就是最终宽高。当然,如果对此重写了,就会不一致了。

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

流程图

在此展示绘制的 layout 过程的流程图,希望能帮助理解该过程。

参考

View.java - Android Code Search

ViewGroup.java - Android Code Search

LinearLayout.java - Android Code Search

《Android进阶之光》

《Android开发艺术探索》