反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里 。html
Android
自己的View
体系很是宏大,源码中值得思考和借鉴之处众多,以View
自己的绘制流程为例,其通过measure
测量、layout
布局、draw
绘制三个过程,最终才可以将其绘制出来并展现在用户面前。android
相比 测量流程 ,布局流程 相对简单不少,若是读者不了解 测量流程 ,建议阅读这篇文章:git
反思 | Android View机制设计与实现:测量流程github
测量流程 的目的是 测量控件宽高 ,但只获取控件的宽高其实是不够的,对于ViewGroup
而言还须要一套额外的逻辑,负责对全部子控件进行对应策略的布局,这就是 布局流程(layout)。markdown
View
而言,其自己没有子控件,所以通常状况下仅须要记录本身在父控件的位置信息,并不须要处理为子控件布局的逻辑;Android
中布局流程中也使用了递归思想:对于一个完整的界面而言,每一个页面都映射了一个View
树,其最顶端的父控件开始布局时,会经过自身的布局策略依次计算出每一个子控件的位置——值得一提的是,为了保证控件树形结构的 内部自治性,每一个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置。位置计算完毕后,做为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当全部控件都布局完毕,整个布局流程结束。对于布局流程不甚熟悉的开发者而言,上述文字彷佛晦涩难懂,但这些文字的归纳其本质倒是布局流程总体的设计思想,读者不该该将本文视为源码分析,而应该将本身代入到设计的过程当中 ,当深入理解整个流程的设计思路以后,布局流程代码地设计和编写天然行云流水一鼓作气。app
首先思考一个问题,布局流程的本质是测量结束以后,将每一个子控件分配到对应的位置上去——既然有子控件,那说明进行布局流程的主体理应是ViewGroup
,那么做为叶子节点的单个View
来讲,为何也会有布局流程呢?框架
读者认真思考能够得出,布局流程其实是一个复杂的过程,整个流程主要逻辑顺序以下:ide
onMeasure()
;整个布局过程当中,除了4是ViewGroup
自身须要作的,其它逻辑对于View
和ViewGroup
而言都是公共的——这说明单个View
也是有布局流程的需求的。函数
如今将整个布局过程定义三个重要的函数,分别为:oop
void layout(int l, int t, int r, int b)
:控件自身整个布局流程的函数;void onLayout(boolean changed, int left, int top, int right, int bottom)
:ViewGroup布局逻辑的函数,开发者须要本身实现自定义布局逻辑;void setFrame(int left, int top, int right, int bottom)
:保存最新布局位置信息的函数;为何须要定义这样三个函数?
如今咱们站在单个View
的角度,首先父控件须要经过调用子控件的layout()
函数,并同时将子控件的位置(left、right、top、bottom
)做为参数传入,标志子控件自己布局流程的开始:
// 伪代码实现 public void layout(int l, int t, int r, int b) { // 1.决定是否须要从新进行测量流程(onMeasure) if(needMeasureBeforeLayout) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec) } // 先将以前的位置信息进行保存 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 2.将自身所在的位置信息进行保存; // 3.判断本次布局流程是否引起了布局的改变; boolean changed = setFrame(l, t, r, b); if (changed) { // 4.若布局发生了改变,令全部子控件从新布局; onLayout(changed, l, t, r, b); // 5.若布局发生了改变,通知全部观察布局改变的监听发送通知 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } 复制代码
这里笔者经过伪代码的方式对布局流程进行了描述,实际上View
自己的layout()
函数内部虽然多处不一样,但核心思想是一致的——layout()
函数实际上表明了控件自身布局的整个流程,setFrame()
和onLayout()
函数都是layout()
中的一个步骤。
为何须要保存布局信息?由于咱们老是有获取控件的宽和高的需求——好比接下来的onDraw()
绘制阶段;而保存了布局信息,就能经过这些值计算控件自己的宽高:
public final int getWidth() { return mWidth; } public final int getHeight() { return mHeight; } 复制代码
因而可知,保存控件的布局信息确实颇有必要,Android中将layout()
函数的四个参数所表明的位置信息,交给了setFrame()
函数去保存:
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; mLeft = left; mTop = top; mRight = right; mBottom = bottom; } return changed; } 复制代码
setFrame()
函数被protected
修饰,这意味着开发者能够经过重写该函数来定义View
自己保存布局信息的逻辑,如今将目光转到mLeft、mTop、mRight、mBottom
四个变量上。
顾名思义,这四个变量对应的天然是View
自身所在的位置,那么View
是如何经过这四个变量描述控件的位置信息呢?
经过一张图来看一下这四个变量所表明的意义:
这时候不可避免的会面临另一个问题,这个mLeft、mTop、mRight、mBottom
的值所对应的坐标系是哪里呢?
这里须要注意的是,为了保证控件树形结构的 内部自治性,每一个子控件的位置为 相对于父控件坐标系的相对位置 ,而不是以屏幕坐标系为准的绝对位置:
反过来想,若是这些位置信息是以屏幕坐标系为准,那么就意味着每一个叶子节点的
View
会持有保存从根节点ViewGroup
直到自身父ViewGroup
每一个控件的位置信息,在计算布局时则更为繁琐,很明显是不合理的设计。
既然View
自身持有了这样的位置信息,实际上前文中获取控件自身宽高的getWidth()
和getHeight()
方法就能够从新这样定义:
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; } 复制代码
这也说明了在布局流程中的setFrame()
函数执行完毕后(且布局确实发生了改变),开发者才能经过getWidth()
和getHeight()
方法获取控件正确的宽高值。
对于叶子节点的View
而言,其并无子控件,所以通常状况下并无为子控件布局的意义(特殊状况请参考AppCompatTextView
等类),所以View
的onLayout()
函数被设计为一个空的实现:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } 复制代码
而在ViewGroup
中,不一样类型的ViewGroup
有不一样的布局策略,这些布局策略的逻辑各不相同,所以该方法被设计为抽象接口,开发者必须实现这个方法以定义ViewGroup
的布局策略:
@Override protected abstract void onLayout(boolean changed,int l, int t, int r, int b); 复制代码
以
LinearLayout
为例,其布局策略为 根据排布方向,将其全部子控件按照指定方向依次排列布局
至此单个View
的测量流程结束,关于ViewGroup
的onLayout
函数细节将在下文进行描述。
相比较测量流程,布局流程相对比较简单,总体思路是,对于一个完整的界面而言,每一个页面都映射了一个View
树,最顶端的父控件开始布局时,会经过自身的布局策略依次计算出每一个子控件的位置。位置计算完毕后,做为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当全部控件都布局完毕,整个布局流程结束。
ViewGroup
虽然重写了View
的layout()
函数,但实质上并未进行大的变更,咱们大抵能够认为ViewGroup
和View
的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 { mLayoutCalledWhileSuppressed = true; } } 复制代码
惟一须要注意的是,开发者必须实现onLayout()
函数以定义ViewGroup
的布局策略,这里以 竖直布局 的LinearLayout
的伪代码为例:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTop; int childLeft; // 遍历全部子View for (int i = 0; i < count; i++) { // 获取子View final View child = getVirtualChildAt(i); // 获取子View宽高,注意这里使用的是 getMeasuredWidth 而不是 getWidth final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); // 令全部子控件开始布局 setChildFrame(child, childLeft, childTop, childWidth, childHeight); // 高度累加,下一个子View的 top 就等于上一个子View的 bottom ,符合竖直线性布局从上到下的布局策略 childTop += childHeight; } } private void setChildFrame(View child, int left, int top, int width, int height) { // 这里能够看到,子控件的mRight实际上就是 mLeft + getMeasuredWidth() // 而在getWidth()函数中,mRight-mLeft的结果就是getMeasuredWidth() // 所以,getWidth() 和 getMeasuredWidth() 是一致的 child.layout(left, top, left + width, top + height); } 复制代码
读者须要注意到一个细节,子控件的宽度的获取,咱们并未使用getWidth()
,而是使用了getMeasuredWidth()
,这就引起了另一个疑问,这两个函数的区别在哪里。
首先,从上文中咱们得知,getWidth()
和getHeight()
函数的相关信息其实是在setFrame()
函数执行完毕才准备完毕的——咱们大体能够认为是这两个函数 只有布局流程(layout)执行完毕才能调用,而在父控件的onLayout()
函数中,获取子控件宽度和高度时,子控件还并未开始进行布局流程,所以此时不能调用getWidth()
函数,而只能经过getMeasuredWidth()
函数获取控件测量阶段结果的宽度。
那么当控件绘制流程执行完毕后,getWidth()
和getMeasuredWidth()
函数的值有什么区别呢?从上述setChildFrame()
函数中的源码能够得知,布局流程执行后,getWidth()
返回值的本质其实就是getMeasuredWidth()
——所以本质上,当咱们没有手动调用layout()
函数强制修改控件的布局信息的话,两个函数的返回值大小是彻底一致的。
在整个布局流程的设计中,设计者将流程中公共的业务逻辑(保存布局信息、通知布局发生改变的监听等)经过layout()
函数进行了整合,同时,将ViewGroup
额外须要的自定义布局策略经过onLayout()
函数向外暴露出来,针对组件中代码的可复用性和可扩展性进行了合理的设计。
至此,布局流程总体实现完毕。借用 carson_ho 绘制的流程图对总体布局流程作一个总结:
Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?