深入理解Android中View绘制的三大流程
前言
最近对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开发艺术探索]
上一篇: 王老师脚扭伤了