Android View 布局流程(Layout)全面解析
前言
,笔者详细讲述了view三大工作流程的第一个,measure流程,如果对测量流程还不熟悉的读者可以参考一下上一篇文章。测量流程主要是对view树进行测量,获取每一个view的测量宽高,那么有了测量宽高,就是要进行布局流程了,布局流程相对测量流程来说简单许多。那么我们开始对layout流程进行详细的解析。
viewgroup的布局流程
上一篇文章提到,三大流程始于viewrootimpl#performtraversals方法,在该方法内通过调用performmeasure、performlayout、performdraw这三个方法来进行measure、layout、draw流程,那么我们就从performlayout方法开始说,我们先看它的源码:
private void performlayout(windowmanager.layoutparams lp, int desiredwindowwidth, int desiredwindowheight) { mlayoutrequested = false; mscrollmaychange = true; minlayout = true; final view host = mview; if (debug_orientation || debug_layout) { log.v(tag, "laying out " + host + " to (" + host.getmeasuredwidth() + ", " + host.getmeasuredheight() + ")"); } trace.tracebegin(trace.trace_tag_view, "layout"); try { host.layout(0, 0, host.getmeasuredwidth(), host.getmeasuredheight()); // 1 //省略... } finally { trace.traceend(trace.trace_tag_view); } minlayout = false; }
由上面的代码可以看出,直接调用了①号的host.layout方法,host也就是decorview,那么对于decorview来说,调用layout方法,就是对它自身进行布局,注意到传递的参数分别是0,0,host.getmeasuredwidth,host.getmeasuredheight,它们分别代表了一个view的上下左右四个位置,显然,decorview的左上位置为0,然后宽高为它的测量宽高。由于view的layout方法是final类型,子类不能重写,因此我们直接看view#layout方法即可:
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 mprivateflags &= ~pflag_layout_required; listenerinfo li = mlistenerinfo; if (li != null && li.monlayoutchangelisteners != null) { arraylist<onlayoutchangelistener> listenerscopy = (arraylist<onlayoutchangelistener>)li.monlayoutchangelisteners.clone(); int numlisteners = listenerscopy.size(); for (int i = 0; i < numlisteners; ++i) { listenerscopy.get(i).onlayoutchange(this, l, t, r, b, oldl, oldt, oldr, oldb); } } } mprivateflags &= ~pflag_force_layout; mprivateflags3 |= pflag3_is_laid_out; }
首先看①号代码,调用了setframe方法,并把四个位置信息传递进去,这个方法用于确定view的四个顶点的位置,即初始化mleft,mright,mtop,mbottom这四个值,当初始化完毕后,viewgroup的布局流程也就完成了
那么,我们先看view#setframe方法:
protected boolean setframe(int left, int top, int right, int bottom) { //省略... mleft = left; mtop = top; mright = right; mbottom = bottom; mrendernode.setlefttoprightbottom(mleft, mtop, mright, mbottom); //省略... return changed; }
可以看出,它对mleft、mtop、mright、mbottom这四个值进行了初始化,对于每一个view,包括viewgroup来说,以上四个值保存了viwe的位置信息,所以这四个值是最终宽高,也即是说,如果要得到view的位置信息,那么就应该在layout方法完成后调用getleft()、gettop()等方法来取得最终宽高,如果是在此之前调用相应的方法,只能得到0的结果,所以一般我们是在onlayout方法中获取view的宽高信息。
在设置viewgroup自身的位置完成后,我们看到会接着调用②号方法,即onlayout()方法,该方法在viewgroup中调用,用于确定子view的位置,即在该方法内部,子view会调用自身的layout方法来进一步完成自身的布局流程。由于不同的布局容器的onmeasure方法均有不同的实现,因此不可能对所有布局方式都说一次,另外上一篇文章是用framelayout#onmeasure进行讲解的,那么现在也对framelayout#onlayout方法进行讲解:
@override protected void onlayout(boolean changed, int left, int top, int right, int bottom) { //把父容器的位置参数传递进去 layoutchildren(left, top, right, bottom, false /* no force left gravity */); } void layoutchildren(int left, int top, int right, int bottom, boolean forceleftgravity) { final int count = getchildcount(); //以下四个值会影响到子view的布局参数 //parentleft由父容器的padding和foreground决定 final int parentleft = getpaddingleftwithforeground(); //parentright由父容器的width和padding和foreground决定 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(); //获取子view的测量宽高 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; //当子view设置了水平方向的layout_gravity属性时,根据不同的属性设置不同的childleft //childleft表示子view的 左上角坐标x值 switch (absolutegravity & gravity.horizontal_gravity_mask) { /* 水平居中,由于子view要在水平中间的位置显示,因此,要先计算出以下: * (parentright - parentleft -width)/2 此时得出的是父容器减去子view宽度后的 * 剩余空间的一半,那么再加上parentleft后,就是子view初始左上角横坐标(此时正好位于中间位置), * 假如子view还受到margin约束,由于leftmargin使子view右偏而rightmargin使子view左偏,所以最后 * 是 +leftmargin -rightmargin . */ case gravity.center_horizontal: childleft = parentleft + (parentright - parentleft - width) / 2 + lp.leftmargin - lp.rightmargin; break; //水平居右,子view左上角横坐标等于 parentright 减去子view的测量宽度 减去 margin case gravity.right: if (!forceleftgravity) { childleft = parentright - width - lp.rightmargin; break; } //如果没设置水平方向的layout_gravity,那么它默认是水平居左 //水平居左,子view的左上角横坐标等于 parentleft 加上子view的magin值 case gravity.left: default: childleft = parentleft + lp.leftmargin; } //当子view设置了竖直方向的layout_gravity时,根据不同的属性设置同的childtop //childtop表示子view的 左上角坐标的y值 //分析方法同上 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; } //对子元素进行布局,左上角坐标为(childleft,childtop),右下角坐标为(childleft+width,childtop+height) child.layout(childleft, childtop, childleft + width, childtop + height); } } }
由源码看出,onlayout方法内部直接调用了layoutchildren方法,而layoutchildren则是具体的实现。
先梳理一下以上逻辑:首先先获取父容器的padding值,然后遍历其每一个子view,根据子view的layout_gravity属性、子view的测量宽高、父容器的padding值、来确定子view的布局参数,然后调用child.layout方法,把布局流程从父容器传递到子元素。
那么,现在就分析完了viewgroup的布局流程,那么我们接着分析子元素的布局流程。
子view的布局流程
子view的布局流程也很简单,如果子view是一个viewgroup,那么就会重复以上步骤,如果是一个view,那么会直接调用view#layout方法,根据以上分析,在该方法内部会设置view的四个布局参数,接着调用onlayout方法,我们看看view#onlayout方法:
protected void onlayout(boolean changed, int left, int top, int right, int bottom) { }
这是一个空实现,主要作用是在我们的自定义view中重写该方法,实现自定义的布局逻辑。
那么到目前为止,view的布局流程就已经全部分析完了。可以看出,布局流程的逻辑相比测量流程来说,简单许多,获取一个view的测量宽高是比较复杂的,而布局流程则是根据已经获得的测量宽高进而确定一个view的四个位置参数。在下一篇文章,将会讲述最后一个流程:绘制流程。希望这篇文章给大家对view的工作流程的理解带来帮助,谢谢阅读。
更多阅读
android view 测量流程(measure)完全解析
android view 绘制流程(draw) 完全解析
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Android View 绘制流程(Draw)全面解析
-
Android View 布局流程(Layout)全面解析
-
Android View 测量流程(Measure)全面解析
-
Android View measure layout draw 过程解析
-
全面解析Android系统指纹启动流程
-
带你全面解析Android框架体系架构view篇 android架构view
-
Android View measure layout draw 过程解析
-
Android开发学习笔记——自定义View(一)View三大工作流程measure、layout和draw
-
带你全面解析Android框架体系架构view篇 android架构view
-
全面解析Android系统指纹启动流程