当前位置: 移动技术网 > 移动技术>移动开发>Android > Android View 布局流程(Layout)全面解析

Android View 布局流程(Layout)全面解析

2019年07月24日  | 移动技术网移动技术  | 我要评论

前言

,笔者详细讲述了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) 完全解析

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网