当前位置: 移动技术网 > 移动技术>移动开发>Android > 深入理解Android中View绘制的三大流程

深入理解Android中View绘制的三大流程

2019年07月24日  | 移动技术网移动技术  | 我要评论
前言 最近对android中view的绘制机制有了一些新的认识,所以想记录下来并分享给大家。view的工作流程主要是指measure、layout、draw这三大流程

前言

最近对android中view的绘制机制有了一些新的认识,所以想记录下来并分享给大家。view的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定view的测量宽高,layout根据测量的宽高确定view在其父view中的四个顶点的位置,而draw则将view绘制到屏幕上,这样通过viewgroup的递归遍历,一个view树就展现在屏幕上了。

说的简单,下面带大家一步一步从源码中分析:

android的view是树形结构的:


基本概念

在介绍view的三大流程之前,我们必须先介绍一些基本的概念,才能更好地理解这整个过程。

window的概念

window表示的是一个窗口的概念,它是站在windowmanagerservice角度上的一个抽象的概念,android中所有的视图都是通过window来呈现的,不管是activity、dialog还是toast,只要有view的地方就一定有window。

这里需要注意的是,这个抽象的window概念和phonewindow这个类并不是同一个东西,phonewindow表示的是手机屏幕的抽象,它充当activity和decorview之间的媒介,就算没有phonewindow也是可以展示view的。

抛开一切,仅站在windowmanagerservice的角度上,android的界面就是由一个个window层叠展现的,而window又是一个抽象的概念,它并不是实际存在的,它是以view的形式存在,这个view就是decorview。

关于window这方面的内容,我们这里先了解一个大概

decorview的概念

decorview是整个window界面的最顶层view,view的测量、布局、绘制、事件分发都是由decorview往下遍历这个view树。decorview作为顶级view,一般情况下它内部会包含一个竖直方向的linearlayout,在这个linearlayout里面有上下两个部分(具体情况和android的版本及主题有关),上面是【标题栏】,下面是【内容栏】。在activity中我们通过setcontentview所设置的布局文件其实就是被加载到【内容栏】中的,而内容栏的id是content,因此指定布局的方法叫setcontent().

viewroot的概念

viewroot对应于viewrootimpl类,它是连接windowmanager和decorview的纽带,view的三大流程均是通过viewroot来完成的。在activitythread中,当activity对象被创建完之后,会讲decorview添加到window中,同时会创建对应的viewrootimpl,并将viewrootimpl和decorview建立关联,并保存到windowmanagerglobal对象中。

windowmanagerglobal.java

root = new viewrootimpl(view.getcontext(), display); 
root.setview(view, wparams, panelparentview);

view的绘制流程是从viewroot的performtraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个view绘制出来,大致流程如下图:

measure测量

为了更好地理解view的测量过程,我们还需要理解measurespec,它是view的一个内部类,它表示对view的测量规格。measurespec代表一个32位int值,高2位代表specmode(测量模式),低30位代表specsize(测量大小),我们可以看看它的具体实现:

measurespec.java

public static class measurespec { 
 private static final int mode_shift = 30;
 private static final int mode_mask = 0x3 << mode_shift;

 /**
  * unspecified 模式:
  * 父view不对子view有任何限制,子view需要多大就多大
  */ 
 public static final int unspecified = 0 << mode_shift;

 /**
  * exactyly 模式:
  * 父view已经测量出子viwe所需要的精确大小,这时候view的最终大小
  * 就是specsize所指定的值。对应于match_parent和精确数值这两种模式
  */ 
 public static final int exactly = 1 << mode_shift;

 /**
  * at_most 模式:
  * 子view的最终大小是父view指定的specsize值,并且子view的大小不能大于这个值,
  * 即对应wrap_content这种模式
  */ 
 public static final int at_most = 2 << mode_shift;

 //将size和mode打包成一个32位的int型数值
 //高2位表示specmode,测量模式,低30位表示specsize,某种测量模式下的规格大小
 public static int makemeasurespec(int size, int mode) {
  if (susebrokenmakemeasurespec) {
  return size + mode;
  } else {
  return (size & ~mode_mask) | (mode & mode_mask);
  }
 }

 //将32位的measurespec解包,返回specmode,测量模式
 public static int getmode(int measurespec) {
  return (measurespec & mode_mask);
 }

 //将32位的measurespec解包,返回specsize,某种测量模式下的规格大小
 public static int getsize(int measurespec) {
  return (measurespec & ~mode_mask);
 }
 //...
 }

measurespec通过将specmode和specsize打包成一个int值来避免过多的对象内存分配,并提供了打包和解包的方法。

specmode有三种类型,每一类都表示特殊的含义:

unspecified

父容器不对view有任何限制,要多大就给多大,这种情况一般用于系统内部,表示一种测量的状态;

exactly

父容器已经检测出view所需的精确大小,这个时候view的最终打消就是specsize所指定的值。它对应于layoutparams中的match_parent和具体数值这两种模式。

at_most

父容器指定了一个可用大小即specsize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现。它对应于layoutparams中wrap_content。

view的measurespec是由父容器的measurespec和自己的layoutparams决定的,但是对于decorview来说有点不同,因为它没有父类。在viewrootimpl中的measurehierarchy方法中有如下一段代码展示了decorview的measurespec的创建过程,其中desiredwindowwidth和desirewindowheight是屏幕的尺寸大小:

viewgroup的measure

childwidthmeasurespec = getrootmeasurespec(desiredwindowwidth, lp.width); 
childheightmeasurespec = getrootmeasurespec(desiredwindowheight, lp.height); 
performmeasure(childwidthmeasurespec, childheightmeasurespec); 

再看看getrootmeasurespec方法:

 private static int getrootmeasurespec(int windowsize, int rootdimension) {
 int measurespec;
 switch (rootdimension) {

 case viewgroup.layoutparams.match_parent:
  // window can't resize. force root view to be windowsize.
  measurespec = measurespec.makemeasurespec(windowsize, measurespec.exactly);
  break;
 case viewgroup.layoutparams.wrap_content:
  // window can resize. set max size for root view.
  measurespec = measurespec.makemeasurespec(windowsize, measurespec.at_most);
  break;
 default:
  // window wants to be an exact size. force root view to be that size.
  measurespec = measurespec.makemeasurespec(rootdimension, measurespec.exactly);
  break;
 }
 return measurespec;
 }

通过以上代码,decorview的measurespec的产生过程就很明确了,因为decorview是framelyaout的子类,属于viewgroup,对于viewgroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和view不同的是,viewgroup是一个抽象类,他没有重写view的onmeasure方法,这里很好理解,因为每个具体的viewgroup实现类的功能是不同的,如何测量应该让它自己决定,比如linearlayout和relativelayout。

因此在具体的viewgroup中需要遍历去测量子view,这里我们看看viewgroup中提供的测量子view的measurechildwithmargins方法:

 protected void measurechildwithmargins(view child,
  int parentwidthmeasurespec, int widthused,
  int parentheightmeasurespec, int heightused) {
 final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams();

 final int childwidthmeasurespec = getchildmeasurespec(parentwidthmeasurespec,
  mpaddingleft + mpaddingright + lp.leftmargin + lp.rightmargin
   + widthused, lp.width);
 final int childheightmeasurespec = getchildmeasurespec(parentheightmeasurespec,
  mpaddingtop + mpaddingbottom + lp.topmargin + lp.bottommargin
   + heightused, lp.height);

 child.measure(childwidthmeasurespec, childheightmeasurespec);
 }

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getchildmeasurespec方法来得到子元素的measurespec。从代码上看,子元素的measurespec的创建与父容器的measurespec和本身的layoutparams有关,此外和view的margin和父类的padding有关,现在看看getchildmeasurespec的具体实现:

viewgroup.java

public static int getchildmeasurespec(int spec, int padding, int childdimension) { 
 int specmode = measurespec.getmode(spec);
 int specsize = measurespec.getsize(spec);

 int size = math.max(0, specsize - padding);

 int resultsize = 0;
 int resultmode = 0;

 switch (specmode) {
 // parent has imposed an exact size on us
 case measurespec.exactly:
 if (childdimension >= 0) {
  resultsize = childdimension;
  resultmode = measurespec.exactly;
 } else if (childdimension == layoutparams.match_parent) {
  // child wants to be our size. so be it.
  resultsize = size;
  resultmode = measurespec.exactly;
 } else if (childdimension == layoutparams.wrap_content) {
  // child wants to determine its own size. it can't be
  // bigger than us.
  resultsize = size;
  resultmode = measurespec.at_most;
 }
 break;

 // parent has imposed a maximum size on us
 case measurespec.at_most:
 if (childdimension >= 0) {
  // child wants a specific size... so be it
  resultsize = childdimension;
  resultmode = measurespec.exactly;
 } else if (childdimension == layoutparams.match_parent) {
  // child wants to be our size, but our size is not fixed.
  // constrain child to not be bigger than us.
  resultsize = size;
  resultmode = measurespec.at_most;
 } else if (childdimension == layoutparams.wrap_content) {
  // child wants to determine its own size. it can't be
  // bigger than us.
  resultsize = size;
  resultmode = measurespec.at_most;
 }
 break;

 // parent asked to see how big we want to be
 case measurespec.unspecified:
 if (childdimension >= 0) {
  // child wants a specific size... let him have it
  resultsize = childdimension;
  resultmode = measurespec.exactly;
 } else if (childdimension == layoutparams.match_parent) {
  // child wants to be our size... find out how big it should
  // be
  resultsize = view.susezerounspecifiedmeasurespec ? 0 : size;
  resultmode = measurespec.unspecified;
 } else if (childdimension == layoutparams.wrap_content) {
  // child wants to determine its own size.... find out how
  // big it should be
  resultsize = view.susezerounspecifiedmeasurespec ? 0 : size;
  resultmode = measurespec.unspecified;
 }
 break;
 }
 //noinspection resourcetype
 return measurespec.makemeasurespec(resultsize, resultmode);
}

上述代码根据父类的measurespec和自身的layoutparams创建子元素的measurespec,具体过程同学们自行分析,最终的创建规则如下表:

viewgroup在遍历完子view后,需要根据子元素的测量结果来决定自己最终的测量大小,并调用setmeasureddimension方法保存测量宽高值。

setmeasureddimension(resolvesizeandstate(maxwidth, widthmeasurespec, childstate),heightsizeandstate); 

这里调用了resolvesizeandstate来确定最终的大小,主要是保证测量的大小不能超过父容器的最大剩余空间maxwidth,这里我们看看它里面的实现:

 public static int resolvesizeandstate(int size, int measurespec, int childmeasuredstate) {
 final int specmode = measurespec.getmode(measurespec);
 final int specsize = measurespec.getsize(measurespec);
 final int result;
 switch (specmode) {
  case measurespec.at_most:
  if (specsize < size) {
   result = specsize | measured_state_too_small;
  } else {
   result = size;
  }
  break;
  case measurespec.exactly:
  result = specsize;
  break;
  case measurespec.unspecified:
  default:
  result = size;
 }
 return result | (childmeasuredstate & measured_state_mask);
 }

关于具体viewgroup的onmeasure过程这里不做分析,由于每种布局的测量方式不一样,不可能逐个分析,但在它们的onmeasure里面的步骤是有一定规律的:

      1.根据各自的测量规则遍历children元素,调用getchildmeasurespec方法得到child的measurespec;

      2.调用child的measure方法;

      3.调用setmeasureddimension确定最终的大小。

view的measure

view的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在view的measure方法里面会去调用onmeasure方法,我们这里只要看onmeasure的实现即可,如下:

view.java

 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec),
  getdefaultsize(getsuggestedminimumheight(), heightmeasurespec));
 }

代码很简单,我们继续看看getdefaultsize方法的实现:

view.java

 public static int getdefaultsize(int size, int measurespec) {
 int result = size;
 int specmode = measurespec.getmode(measurespec);
 int specsize = measurespec.getsize(measurespec);

 switch (specmode) {
 case measurespec.unspecified:
  result = size;
  break;
 case measurespec.at_most:
 case measurespec.exactly:
  result = specsize;
  break;
 }
 return result;
 }

从上述代码可以得出,view的宽/高由specsize决定,直接继承view的自定义控件需要重写onmeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent

上述就是view的measure大致过程,在measure完成之后,通过getmeasuredwidth/height方法就可以获得测量后的宽高,这个宽高一般情况下就等于view的最终宽高了,因为view的layout布局的时候就是根据measurewidth/height来设置宽高的,除非在layout中修改了measure值。

layout布局

layout的作用是viewgroup用来确定子元素的位置,当viewgroup的位置被确定后,它在onlayout中会遍历所有的子元素并调用其layout方法。简单的来说就是,layout方法确定view本身的位置,而onlayout方法则会确定所有子元素的位置。

先看看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);

  if (changed || (mprivateflags & pflag_layout_required) == pflag_layout_required) {
   onlayout(changed, l, t, r, b);

   if (shoulddrawroundscrollbar()) {
    if(mroundscrollbarrenderer == null) {
     mroundscrollbarrenderer = new roundscrollbarrenderer(this);
    }
   } else {
    mroundscrollbarrenderer = null;
   }

   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;
 }

主要看到这里:

boolean changed = islayoutmodeoptical(mparent) ?setopticalframe(l, t, r, b) : setframe(l, t, r, b); 

islayoutmodeoptical方法判断是否显示边界布局(这个东西不知道是啥,暂时不理会),setopticalframe方法内部最终也是调用setframe方法,这里我们看setframe方法就可以了:

 protected boolean setframe(int left, int top, int right, int bottom) {
  boolean changed = false;

  if (dbg) {
   log.d("view", this + " view.setframe(" + left + "," + top + ","
  + right + "," + bottom + ")");
  }
  //1、如果有一个值发生了改变,那么就需要重新调用onlayout方法了,后面会分析到
  if (mleft != left || mright != right || mtop != top || mbottom != bottom) {
   changed = true;

   // remember our drawn bit
   int drawn = mprivateflags & pflag_drawn;

   //2、保存旧的宽和高
   int oldwidth = mright - mleft;
   int oldheight = mbottom - mtop;
   //计算新的宽和高
   int newwidth = right - left;
   int newheight = bottom - top;
   //3、判断宽高是否有分生变化
   boolean sizechanged = (newwidth != oldwidth) || (newheight != oldheight);

   //invalidate our old position
   //4、如果大小变化了,在已绘制了的情况下就请求重新绘制
   invalidate(sizechanged);

   //5、存储新的值
   mleft = left;
   mtop = top;
   mright = right;
   mbottom = bottom;
   mrendernode.setlefttoprightbottom(mleft, mtop, mright, mbottom);

   mprivateflags |= pflag_has_bounds;

   if (sizechanged) {
   //6、大小变化时进行处理
   sizechange(newwidth, newheight, oldwidth, oldheight);
   }

   if ((mviewflags & visibility_mask) == visible || mghostview != null) {
   //7、如果此时view是可见状态下,立即执行绘制操作
   invalidate(sizechanged);

   }

   mprivateflags |= drawn;

   mbackgroundsizechanged = true;
   if (mforegroundinfo != null) {
   mforegroundinfo.mboundschanged = true;
   }

   notifysubtreeaccessibilitystatechangedifneeded();
  }
  return changed;
 }
  • 首先判断四个顶点的位置是否有变化;
  • 判断宽高是否有变化,如果变化了则请求重新绘制;
  • 保存新的值top、left、bottom、right。

可以看到changed的值只与四个点是否发生了变化有关。同时,我们还发现,在setframe方法后,就可以获得某个view的top、left、right、bottom的值了。

回到layout方法中,继续执行会调用onlayout方法,我们看看其代码:

protected void onlayout(boolean changed, int left, int top, int right, int bottom) {} 

可以看到这是一个空实现,和onmeasure方法类似,onlayout的实现和具体的布局有关,具体viewgroup的子类需要重写onlayout方法,并根据具体布局规则遍历调用children的layout方法。

通过上面的分析,可以得到两个结论:

  • view通过layout方法来确认自己在父容器中的位置
  • viewgroup通过onlayout 方法来确定view在容器中的位置

接下来我们看看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();

  final int parentleft = getpaddingleftwithforeground();
  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();

    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;

    switch (absolutegravity & gravity.horizontal_gravity_mask) {
     case gravity.center_horizontal:
      childleft = parentleft + (parentright - parentleft - width) / 2 +
      lp.leftmargin - lp.rightmargin;
      break;
     case gravity.right:
      if (!forceleftgravity) {
       childleft = parentright - width - lp.rightmargin;
       break;
      }
     case gravity.left:
     default:
      childleft = parentleft + lp.leftmargin;
    }

    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;
    }

    child.layout(childleft, childtop, childleft + width, childtop + height);
   }
  }
 }

1、获取父view的内边距padding的值

2、遍历子view,处理子view的layout_gravity属性、根据view测量后的宽和高、父view的padding值、来确定子view的布局参数,

3、调用child.layout方法,对子view进行布局

draw绘制

draw过程就比较简单了,它的作用是将view绘制到屏幕上面。view的绘制过程遵循如下几部:

  • 绘制背景background.draw(canvas);
  • 绘制自己ondraw;
  • 绘制children:dispatchdraw;
  • 绘制装饰ondrawforeground;

这里我们看看draw方法:

 public void draw(canvas canvas) {
  final int privateflags = mprivateflags;
  final boolean dirtyopaque = (privateflags & pflag_dirty_mask) == pflag_dirty_opaque &&
    (mattachinfo == null || !mattachinfo.mignoredirtystate);
  mprivateflags = (privateflags & ~pflag_dirty_mask) | pflag_drawn;

  /*
   * draw traversal performs several drawing steps which must be executed
   * in the appropriate order:
   *
   *  1. draw the background
   *  2. if necessary, save the canvas' layers to prepare for fading
   *  3. draw view's content
   *  4. draw children
   *  5. if necessary, draw the fading edges and restore layers
   *  6. draw decorations (scrollbars for instance)
   */

  // step 1, draw the background, if needed
  int savecount;

  if (!dirtyopaque) {
   drawbackground(canvas);
  }

  // skip step 2 & 5 if possible (common case)
  final int viewflags = mviewflags;
  boolean horizontaledges = (viewflags & fading_edge_horizontal) != 0;
  boolean verticaledges = (viewflags & fading_edge_vertical) != 0;
  if (!verticaledges && !horizontaledges) {
   // step 3, draw the content
   if (!dirtyopaque) ondraw(canvas);

   // step 4, draw the children
   dispatchdraw(canvas);

   // overlay is part of the content and draws beneath foreground
   if (moverlay != null && !moverlay.isempty()) {
    moverlay.getoverlayview().dispatchdraw(canvas);
   }

   // step 6, draw decorations (foreground, scrollbars)
   ondrawforeground(canvas);

   // we're done...
   return;
  }

   ... ...

 }

view的绘制过程的传递是通过dispatchdraw来实现的,dispatchdraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。

总结

到这里,view的measure、layout、draw三大流程就说完了,这里做一下总结:

如果是自定义viewgroup的话,需要重写onmeasure方法,在onmeasure方法里面遍历测量子元素,同理onlayout方法也是一样,最后实现ondraw方法绘制自己;

如果自定义view的话,则需要从写onmeasure方法,处理wrap_content的情况,不需要处理onlayout,最后实现ondraw方法绘制自己;

好了,以上就是这篇文章的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。

引用[android开发艺术探索]

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网