PS:本文系转载文章,阅读原文可读性会更好,文章末尾有原文链接
ps:文章是基于 Android Api 31来分析源码的。
目录
1、View 的 layout 过程
1、1 View(它不是ViewGroup) 的 layout 过程
1、1、1 原始 View 的 layout 过程
1、1、2 具体 View 的 layout 过程
1、2 ViewGroup 的 layout 过程
1、View 的 layout 过程
layout 的过程是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,如果这个 View 是 ViewGroup 并且有子元素的话,它在onLayout中 会遍历所有的子元素并调用其 layout 方法,在 layout 方法中onLayout方法又会被调用;下面我们先说一下非 ViewGroup 的 layout 过程。
1、1 View(它不是ViewGroup)的 layout 过程
1、1、2 原始 View 的 layout 过程
我们先看一下 View 的 layout 方法;
图片
先看注释2,onLayout 方法是确定当前 View 的所有子元素的位置,我们看看 View 的 onLayout 方法;
图片
看到没,View 的 onLayout 方法是一个空实现,所以直接用 View 这个类作为 xml 文件中的标签的话是没有子元素的。
好,我们现在看一下注释1中的 setFrame 方法,这个方法位于 View 这个类中;
图片
看注释3这4个变量的赋值,left、top、right 和 bottom 就是当前这个 View 的4个顶点的位置,所以说 setFrame 方法就是确定当前 View 的在父容器布局中的位置。
1、1、2 具体 View 的 layout 过程
我这里介绍的具体的 View ,它不是 ViewGroup 哦,它是继承于 View 的子类或者是 View 的子类的子类等等,例如 TextView、ImageView、Button 等;好,我们这里就用 TextView 的 layout 过程来分析,TextView 并没有重写 layout 方法,只是重写了 onLayout 方法;
图片
看 TextView 中的 onLayout 方法,它调用了 super.onLayout 方法,而 super 就是 View,所以 super.onLayout 是空实现;看注释4的方法,它只是自动计算和设置文本大小,所以 TextView 的位置设置是在 View 的 setFrame 方法中,同时证明 TextView 它是不可能作为其他 View 的父容器。
1、2 ViewGroup 的 layout 过程
我们知道,Android 中系统自带的 ViewGroup 实现类有 RelativeLayout、LinearLayout、GridLayout、TableLayout、FrameLayout 和 Constraint-Layout,由于 ViewGroup 子类的布局特性不同,所以 ViewGroup 子类对子元素的位置设置也就不同了;这里我们用 FrameLayout 的 layout 过程进行分析,同样 FrameLayout 没有重写 layout 方法,它的父类 ViewGroup 也没有重写 layout 方法,我们只需看它的 onLayout 方法就好了;
图片
FrameLayout 的 onLayout 方法调用了 FrameLayout 的 layoutChildren 方法,我们往下看 layoutChildren 方法;
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
//5、
final int count = getChildCount();
//6、
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) {
......
//7、
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//8、
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;
}
//9、
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;
}
//10、
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
看注释5,它是获取子元素的个数;看注释6中的 parentLeft、parentRight、parentTop 和 parentBottom,是获取当前的 FrameLayout 左右上下这四个方向的 padding;看注释7,根据 View 中 LayoutParams 的 Gravity 来计算 FrameLayout 子 View 的四个顶点位置,看是左到右还是右到左;看注释8,根据横坐标来计算子 View 的 left 位置;看注释9,根据纵坐标来计算子 View 的 top 位置;看注释10,知道了子 View 的 left、top、width 和 height 后,递归调用子 View 的 layout 过程并顺便对子 View 的位置进行保存。
在Android中View的工作流程之measure过程这篇文章我们有提到过,一般情况下测量的宽高等于最终的宽高对不对?只不过测量宽高形成于 View 的 measure 过程,而最终宽高形成于 View 的 layout 过程,只是这两者的赋值时机不同而已,最终宽高的赋值时机稍微晚一些,所以,在平时开发中,我们可以认为 View 的测量宽高等于最终宽高;但是一些特殊情况下测量宽高和最终宽高不一样,下面我举个例子;
图片
注释11的代码会导致在任何情况下 View 的最终宽高比测量宽高大40 px,
这样会导致 View 显示不正常,这实了测量宽高真的可以不等于最终宽高,但实际开发中,我们不推荐这样做;有时候,View 需要多次 measure 过程才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高很有可能和最终宽高不一样,到最后,测量宽高和最终宽高是一样的。