欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-20 11:33:34
前言 ,笔者详细讲述了view三大工作流程的第一个,measure流程,如果对测量流程还不熟悉的读者可以参考一下上一篇文章。测量流程主要是对view树进行测量,获取每一个...

前言

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

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

上一篇:

下一篇: