Android onLayout()布局流程解析

Android onLayout()布局流程解析

测量与绘制流程文章

Android onMeasure()测量流程解析
Android onDraw()绘制流程解析

1.组件布局流程的那些结论

先看结论再看分析

1.)layout流程始于ViewRootImpl的performLayout()方法,该方法会调用根View(DecorView)的layout()方法进行布局,因为DecorView是ViewGroup(FrameLayout),所以layout流程来到了ViewGroup(其实调用的是父类View类)的layout()方法
2. )layout()方法负责组件自身的布局,会存储mLeft、mTop、mRight、mBottom 四个值来存储自身的位置,如果布局变化,还会回调onSizeChanged()方法通知我们布局发生了变化。最后layout()方法还会调用onLayout()方法进行子组件的布局。
3. )View类因为没有子组件,所以View类的onLayout()方法是个空实现,ViewGroup类因为有子组件,但我们自定义的ViewGroup类都有自己的布局规则,所以ViewGroup类的onLayout()方法是个抽象方法,需要子类去实现,所以我们直接继承ViewGroup类一定需要重写onLayout()方法。
4. )实现ViewGroup的onLayout()方法,我们需要循环所有的子组件,根据我们自己的布局规则,确定他们的位置,然后调用子组件的layout()方法 ,让子组件存储自身的位置,如果子组件是View,则没有后续的布局流程,如果子组件还是ViewGroup,则会重复上述步骤,直到完成所有的布局流程。

2. 源码解析

三大流程始于ViewRootImpl的performTraversals()方法,该方法通过依次调用performMeasure()、performLayout()、performDraw()这三个方法来进行measure、layout、draw流程,所以要了解layout流程我们需要从performLayout方法开始。
我们省略大部分代码,只看我们关注的:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {

    final View host = mView;
try {
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

}

host就是是DecorView,可以看到,调用了的host的layout()方法,layout()方法的作用就是对自身进行布局,传递的参数分别是0,0,host.getMeasuredWidth,host.getMeasuredHeight。
我们知道DecorView的ViewGroup(FrameLayout),所以我们去查看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);
        }
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

其中四个参数分别代表:
l: 组件左边相对于父布局的左边位置
t: 组件上边相对于父布局的上边位置
r: 组件右边相对于父布局的左边位置
b: 组件底边相对于父布局的上边位置
我们知道,我们开发中通过View.getLeft()、getTop()、getRight()、getBottom()四个方法能获取到上面四个值,分别对应View类的四个变量,mLeft、mTop、mRight、mBottom,所以我们可以判断出在布局过程中,会对这四个变量进行赋值。

public final int getLeft() {
    return mLeft;
}
public final int getTop() {
    return mTop;
}
public final int getRight() {
    return mRight;
}
public final int getBottom() {
    return mBottom;
}

可以看到,ViewGroup的layout()方法调用了父类也就是View类的layout()方法,同样的,我们省略大部分代码,只看我们需要关注的部分:

public void layout(int l, int t, int r, int b) {
boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);
}
}

可以看到,先通过
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
判断布局有没有变化,如果有变化就调用了onLayout(changed, l, t, r, b);方法进行布局。因为setOpticalFrame()最后也调用了setFrame()方法,所以我们直接查看setFrame()方法,代码如下:

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (DBG) {
        Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                + right + "," + bottom + ")");
    }

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;


        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(sizeChanged);

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

        mPrivateFlags |= PFLAG_HAS_BOUNDS;


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

代码非常简单,通过传进来的 left, top, right, bottom与以前存过的mLeft、mTop、mRight、mBottom进行比较,如果不一致,表示布局发生了变化,就重新赋值mLeft、mTop、mRight、mBottom,
可以看到如果不一致还调用了sizeChang()方法,sizeChang()方法右调用了onSizeChanged()方法通知我们布局发生了变化。

private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
    onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
}

由上述可以知道:
1.)mLeft、mTop、mRight、mBottom的赋值是在layout()方法中执行的,如果我们想要得到View的位置信息,那么就应该在layout方法完成后调用getLeft()、getTop()等方法来获取,如果是在此之前调用相应的方法,只能得到0。

2.)layout()方法会调用onLayout();方法进行子组件的布局,我们查看View的onLayout()方法,是一个空实现。这是肯定的,因为View没有子组件,不可能进行布局。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

我们继续查看ViewGroup的onLayout()方法,是一个抽象方法,这也是肯定的,因为如果我们想实现一个布局组件,我们一定要根据我们的规则确定子组件的位置。

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

因为根View是FrameLayout,所以我们去查看FrameLayout的onLayout()方法来理解布局的流程,源码如下:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false );
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

逻辑也比较简单,循环所有子View,根据子View的LayoutParams里设置的gravity、margin,FrameLayout自身的padding等确认每个子View的位置,调用子View的layout()方法,进行后续的layout流程。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值