前言
在上篇文章Android View体系 - 测量篇中,我们知道:每个View
都会接收来自父View
的MeasureSpec
来进行测量,在确保自身能够独立完成测量逻辑的同时,也能向下推导,促使其子View
的测量。
与测量流程同为View
三大流程的布局流程,是如何独立完成自身的布局流程,又能向下推导,促使孩子节点的布局的?本文将围绕着View
布局流程的源码,尝试探索View
的布局流程。
View的布局
一棵View
树的布局起源于ViewRootImpl
的performTranversals方法:
1 | // ViewRootImpl.java |
在performTraversals内部会调用performLayout方法,从根View
开始测量:
1 | // ViewRootImpl.java |
在performLayout内部,逻辑大致可以分为两块:
- 执行根
View
的layout方法开始布局; - 处理在布局过程中,当前
View
树的所有requestLayout请求;
第2块不是主流程,主要来看第1块:
1 | // View.java |
在View
的layout方法中,主要的逻辑分为两块:
- setFrame - 为当前
View
设置布局的位置 - onLayout - 为当前
View
的子View
布局
setFrame
先看看View
的setFrame方法。
1 | // View.java |
setFrame方法内部的逻辑相对较为简单,内部逻辑会与上次设置的left、top、right、bottom进行比对,如果发生了改变,就会设置新值,否则啥也不干。
onLayout
再来看看View
的onLayout方法:
1 | // View.java |
可以看到View
的onLayout是一个空实现。这也可以理解,因为onLayout是用来布局子View
的,一个View
不一定会有子View
,ViewGroup
才有可能有。
ViewGroup的布局
ViewGroup
也是View
,不同的是,它能够承载其他View
。ViewGroup
布局的入口也是layout方法,它重写了(也是唯一一个重写layout的View
子类)layout方法:
1 | // ViewGroup.java |
从代码逻辑可以看到,如果一个ViewGroup
没有通过suppressLayout方法来禁用布局,且动画没有在改变布局的位置时,就会调用超类View
的layout方法,因此所做的逻辑大致与其超类View
的基本一致。
onLayout
ViewGroup
的layout方法逻辑与View
的基本一致,View
的layout方法有两个关键的方法:setFrame和onLayout。setFrame方法ViewGroup
没有重写,我们来看看ViewGroup
的onLayout方法:
1 | // ViewGroup.java |
可以看到,ViewGroup
的onLayout方法变成了抽象方法,子类必须实现它,以完成布局ViewGroup
子View
的逻辑。
以FrameLayout
为例:
1 | // FrameLayout.java |
虽然代码很长,但是能看出来这段代码做了三件事:
- 计算每个子
View
的left - 计算每个子
View
的top - 调用子
View
的layout方法布局子View
计算子View
的left依托于FrameLayout
的left、水平方向的gravity(左对齐、水平居中、右对齐)
计算子View
的top依托于FrameLayout
的top、竖直方向的gravity(顶部对齐、竖直居中、底部对齐)
计算完子View
的left和top后,依据子View
的measuredWidth和measuredHeight,就能计算出子View
的right和bottom,因此就能调用子View
的layout方法为子View
进行布局。
坐标系、padding和margin
通读完View
、ViewGroup
和FrameLayout
的布局代码,left、top、right、bottom这几个代表什么意思?
如果把一个View
理解成一个矩形:
- left - 代表这个矩形的左边与父
View
左边的距离 - top - 代表这个矩形的上边与父
View
上边的距离 - right - 代表这个矩形的右边与父
View
左边的距离 - bottom - 代表这个矩形的下边与父
View
上边的距离
可以看到,left、top、right、bottom是基于子View
相对于父View
的相对坐标系上的定义。相对坐标系的好处在于
- 父
View
可以对子View
的布局位置进行限制; - 相比于绝对坐标系(以屏幕为参考系),相对坐标系在递归时更容易对参数进行处理
此外,在View
、ViewGroup
和FrameLayout
的布局代码中,还出现了padding和margin:
- padding - 父
View
的空出的位置,压缩子View
的布局空间 - margin - 子
View
主动让出的空间
一图胜千言:
View树的布局
了解了坐标系、padding和margin后,就可以从FrameLayout
的布局代码,还原出整棵View
树的布局流程:
- 父
View
调用setFrame为自己圈定布局的矩形范围; - 以父
View
的left、top作为原点,根据padding将子View
的布局限定在一个矩形区域内; - 在这个矩形区域内,根据子
View
的margin、measuredWidth和measuredHeight,布局子View
;
总而言之,一棵View
树的布局流程如图:
总结
View
布局的总体流程相较于View
测量的而言较为简单,主要分为是否有子View
这两种情况:
- 有子
View
-ViewGroup
会首先调用setFrame圈住一个(left, top, right, bottom)的矩形区域,再通过onLayout调用子View
的layout方法为子View
布局; - 无子
View
- 没有子View
的View
只会调用setFrame方法,然后调用一般为空实现的onLayout方法。
而解决画在哪这个问题的关键在于setFrame方法,setFrame方法的参数left、top、right和bottom是建立在以当前View
的父View
为参考系的相对坐标系,从而使得View
能够无需关心屏幕坐标及屏幕尺寸,能够在父View
给定的(left, top, right, bottom)的约束下进行布局。