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

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

程序员文章站 2022-05-02 21:04:48
前言 最近对android中view的绘制机制有了一些新的认识,所以想记录下来并分享给大家。view的工作流程主要是指measure、layout、draw这三大流程...

前言

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

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

android的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().

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

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绘制出来,大致流程如下图:

深入理解Android中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,具体过程同学们自行分析,最终的创建规则如下表:

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

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开发艺术探索]