13问13答全面学习Android View绘制
本文通过13问13答学习android view绘制,供大家参考,具体内容如下
1.view的绘制流程分几步,从哪开始?哪个过程结束以后能看到view?
答:从viewroot的performtraversals开始,经过measure,layout,draw 三个流程。draw流程结束以后就可以在屏幕上看到view了。
2.view的测量宽高和实际宽高有区别吗?
答:基本上百分之99的情况下都是可以认为没有区别的。有两种情况,有区别。第一种 就是有的时候会因为某些原因 view会多次测量,那第一次测量的宽高 肯定和最后实际的宽高 是不一定相等的,但是在这种情况下
最后一次测量的宽高和实际宽高是一致的。此外,实际宽高是在layout流程里确定的,我们可以在layout流程里 将实际宽高写死 写成硬编码,这样测量的宽高和实际宽高就肯定不一样了,虽然这么做没有意义 而且也不好。
3.view的measurespec 由谁决定?*view呢?
答:由view自己的layoutparams和父容器 一起决定自己的measurespec。一旦确定了spec,onmeasure中就可以确定view的宽高了。
*view就稍微特殊一点,对于decorview的测量在viewrootimpl的源码里。
//desire的这2个参数就代表屏幕的宽高, childwidthmeasurespec = getrootmeasurespec(desiredwindowwidth, lp.width); childheightmeasurespec = getrootmeasurespec(desiredwindowheight, lp.height); performmeasure(childwidthmeasurespec, childheightmeasurespec); //decorview的measurespec就是在这里确定的,其实比普通view的measurespec要简单的多 //代码就不分析了 一目了然的东西 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; }
4.对于普通view来说,他的measure过程中,与父view有关吗?如果有关,这个父view也就是viewgroup扮演了什么角色?
答:看源码:
//对于普通view的measure来说 是由这个view的 父view ,也就是viewgroup来触发的。 //也就是下面这个measurechildwithmargins方法 protected void measurechildwithmargins(view child, int parentwidthmeasurespec, int widthused, int parentheightmeasurespec, int heightused) { //第一步 先取得子view的 layoutparams 参数值 final marginlayoutparams lp = (marginlayoutparams) child.getlayoutparams(); //然后开始计算子view的spec的值,注意这里看到 计算的时候除了要用子view的 layoutparams参数以外 //还用到了父view 也就是viewgroup自己的spec的值 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); } //这个算view的spec的方法 看上去一大串 但是真的逻辑非常简单 就是根据父亲viewgroup //的meaurespec 同时还有view自己的params来确定 view自己的measurespec。 //注意这里的参数是padding,这个值的含义是 父容器已占用的控件的大小 所以view的specsize //的值 你们可以看到 是要减去这个padding的值的。总大小-已经用的 =可用的。 很好理解。 //然后就是下面的switch逻辑 要自己梳理清楚。其实也不难,主要是下面几条原则 //如果view采用固定宽高,也就是写死的数值那种。那就不管父亲的spec的值了,view的spec 就肯定是exactly 并且大小遵循layout参数里设置的大小。 //如果view的宽高是match_parent ,那么就要看父容器viewgroup的 spec的值了,如果父view的spec是exactly模式, //那view也肯定是exactly,并且大小就是父容器剩下的空间。如果父容器是at_most模式,那view也是at_most 并且不会超过剩余空间大小 //如果view的宽高是wrap_content, 那就不管父容器的spec了,view的spec一定是at_most 并且不会超过父view 剩余空间的大小。 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; } return measurespec.makemeasurespec(resultsize, resultmode); }
5.view的meaure和onmeasure有什么关系?
答:看源码:
//view的measure是final 方法 我们子类无法修改的。 public final void measure(int widthmeasurespec, int heightmeasurespec) { boolean optical = islayoutmodeoptical(this); if (optical != islayoutmodeoptical(mparent)) { insets insets = getopticalinsets(); int owidth = insets.left + insets.right; int oheight = insets.top + insets.bottom; widthmeasurespec = measurespec.adjust(widthmeasurespec, optical ? -owidth : owidth); heightmeasurespec = measurespec.adjust(heightmeasurespec, optical ? -oheight : oheight); } // suppress sign extension for the low bytes long key = (long) widthmeasurespec << 32 | (long) heightmeasurespec & 0xffffffffl; if (mmeasurecache == null) mmeasurecache = new longsparselongarray(2); if ((mprivateflags & pflag_force_layout) == pflag_force_layout || widthmeasurespec != moldwidthmeasurespec || heightmeasurespec != moldheightmeasurespec) { // first clears the measured dimension flag mprivateflags &= ~pflag_measured_dimension_set; resolvertlpropertiesifneeded(); int cacheindex = (mprivateflags & pflag_force_layout) == pflag_force_layout ? -1 : mmeasurecache.indexofkey(key); if (cacheindex < 0 || signoremeasurecache) { // measure ourselves, this should set the measured dimension flag back onmeasure(widthmeasurespec, heightmeasurespec); mprivateflags3 &= ~pflag3_measure_needed_before_layout; } else { long value = mmeasurecache.valueat(cacheindex); // casting a long to int drops the high 32 bits, no mask needed setmeasureddimensionraw((int) (value >> 32), (int) value); mprivateflags3 |= pflag3_measure_needed_before_layout; } // flag not set, setmeasureddimension() was not invoked, we raise // an exception to warn the developer if ((mprivateflags & pflag_measured_dimension_set) != pflag_measured_dimension_set) { throw new illegalstateexception("view with id " + getid() + ": " + getclass().getname() + "#onmeasure() did not set the" + " measured dimension by calling" + " setmeasureddimension()"); } mprivateflags |= pflag_layout_required; } moldwidthmeasurespec = widthmeasurespec; moldheightmeasurespec = heightmeasurespec; mmeasurecache.put(key, ((long) mmeasuredwidth) << 32 | (long) mmeasuredheight & 0xffffffffl); // suppress sign extension } //不过可以看到的是在measure方法里调用了onmeasure方法 //所以就能知道 我们在自定义view的时候一定是重写这个方法! protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec), getdefaultsize(getsuggestedminimumheight(), heightmeasurespec)); }
6.简要分析view的measure流程?
答:先回顾问题4,viewgroup 算出子view的spec以后 会调用子view的measure方法,而子view的measure方法 我们问题5也看过了实际上是调用的onmeasure方法
所以我们只要分析好onmeasure方法即可,注意onmeasure方法的参数 正是他的父view算出来的那2个spec的值(这里view的measure方法会把这个spec里的specsize值做略微的修改 这个部分 不做分析 因为measure方法修改specsize的部分很简单)。
//可以看出来这个就是setmeasureddimension方法的调用 这个方法看名字就知道就是确定view的测量宽高的 //所以我们分析的重点就是看这个getdefaultsize 方法 是怎么确定view的测量宽高的 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec), getdefaultsize(getsuggestedminimumheight(), heightmeasurespec)); } //这个方法特别简单 基本可以认为就是近似的返回spec中的specsize,除非你的specmode是unspecified //unspecified 这个一般都是系统内部测量才用的到,这种时候返回size 也就是getsuggestedminimumwidth的返回值 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的背景相关 这里不多做分析了 protected int getsuggestedminimumwidth() { return (mbackground == null) ? mminwidth : max(mminwidth, mbackground.getminimumwidth()); }
7.自定义view中 如果onmeasure方法 没有对wrap_content 做处理 会发生什么?为什么?怎么解决?
答:如果没有对wrap_content做处理 ,那即使你在xml里设置为wrap_content.其效果也和match_parent相同。看问题4的分析。我们可以知道view自己的layout为wrap,那mode就是at_most(不管父亲view是什么specmode).
这种模式下宽高就是等于specsize(getdefaultsize函数分析可知),而这里的specsize显然就是parentsize的大小。也就是父容器剩余的大小。那不就和我们直接设置成match_parent是一样的效果了么?
解决方式就是在onmeasure里 针对wrap 来做特殊处理 比如指定一个默认的宽高,当发现是wrap_content 就设置这个默认宽高即可。
8.viewgroup有onmeasure方法吗?为什么?
答:没有,这个方法是交给子类自己实现的。不同的viewgroup子类 肯定布局都不一样,那onmeasure索性就全部交给他们自己实现好了。
9.为什么在activity的生命周期里无法获得测量宽高?有什么方法可以解决这个问题吗?
答:因为measure的过程和activity的生命周期 没有任何关系。你无法确定在哪个生命周期执行完毕以后 view的measure过程一定走完。可以尝试如下几种方法 获取view的测量宽高。
//重写activity的这个方法 public void onwindowfocuschanged(boolean hasfocus) { super.onwindowfocuschanged(hasfocus); if (hasfocus) { int width = tv.getmeasuredwidth(); int height = tv.getmeasuredheight(); log.v("burning", "width==" + width); log.v("burning", "height==" + height); } }
或者重写这个方法
@override protected void onstart() { super.onstart(); tv.post(new runnable() { @override public void run() { int width = tv.getmeasuredwidth(); int height = tv.getmeasuredheight(); } }); }
再或者:
@override protected void onstart() { super.onstart(); viewtreeobserver observer = tv.getviewtreeobserver(); observer.addongloballayoutlistener(new viewtreeobserver.ongloballayoutlistener() { @override public void ongloballayout() { int width = tv.getmeasuredwidth(); int height = tv.getmeasuredheight(); tv.getviewtreeobserver().removeongloballayoutlistener(this); } }); }
10.layout和onlayout方法有什么区别?
答:layout是确定本身view的位置 而onlayout是确定所有子元素的位置。layout里面 就是通过serframe方法设设定本身view的 四个顶点的位置。这4个位置以确定 自己view的位置就固定了
然后就调用onlayout来确定子元素的位置。view和viewgroup的onlayout方法都没有写。都留给我们自己给子元素布局
11.draw方法 大概有几个步骤?
答: 一共是4个步骤, 绘制背景---------绘制自己--------绘制chrildren----绘制装饰。
12.setwillnotdraw方法有什么用?
答:这个方法在view里。
/** * if this view doesn't do any drawing on its own, set this flag to * allow further optimizations. by default, this flag is not set on * view, but could be set on some view subclasses such as viewgroup. * * typically, if you override {@link #ondraw(android.graphics.canvas)} * you should clear this flag. * * @param willnotdraw whether or not this view draw on its own */ public void setwillnotdraw(boolean willnotdraw) { setflags(willnotdraw ? will_not_draw : 0, draw_mask); }
用于设置标志位的 也就是说 如果你的自定义view 不需要draw的话,就可以设置这个方法为true。这样系统知道你这个view 不需要draw 可以优化执行速度。viewgroup 一般都默认设置这个为true,因为viewgroup多数都是只负责布局
不负责draw的。而view 这个标志位 默认一般都是关闭的。
13.自定义view 有哪些需要注意的点?
答:主要是要处理wrap_content 和padding。否则xml 那边设置这2个属性就根本没用了。还有不要在view中使用handler 因为人家已经提供了post方法。如果是继承自viewgroup,那在onmeasure和onlayout里面 也要考虑
padding和layout的影响。也就是说specsize 要算一下 。最后就是如果view的动画或者线程需要停止,可以考虑在ondetachedfromwindow里面来做。
针对上述的几点,给出几个简单的自定义view 供大家理解。
给出一个圆形的view 范例:
package com.example.administrator.motioneventtest; import android.content.context; import android.graphics.canvas; import android.graphics.color; import android.graphics.paint; import android.util.attributeset; import android.view.view; /** * created by administrator on 2016/2/4. */ public class circleview extends view { private int mcolor = color.red; private paint mpaint = new paint(paint.anti_alias_flag); private void init() { mpaint.setcolor(mcolor); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int widthspecmode = measurespec.getmode(widthmeasurespec); int widthspecsize = measurespec.getsize(widthmeasurespec); int heightspecmode = measurespec.getmode(heightmeasurespec); int heightspecsize = measurespec.getsize(heightmeasurespec); //处理为wrap_content时的情况 if (widthspecmode == measurespec.at_most && heightspecmode == measurespec.at_most) { setmeasureddimension(200, 200); } else if (widthspecmode == measurespec.at_most) { setmeasureddimension(200, heightspecsize); } else if (heightspecmode == measurespec.at_most) { setmeasureddimension(widthspecsize, 200); } } @override protected void ondraw(canvas canvas) { super.ondraw(canvas); //处理padding的情况 final int paddingleft = getpaddingleft(); final int paddingright = getpaddingright(); final int paddingtop = getpaddingtop(); final int paddingbottom = getpaddingbottom(); int width = getwidth() - paddingleft - paddingright; int height = getheight() - paddingtop - paddingbottom; int radius = math.min(width, height) / 2; canvas.drawcircle(paddingleft + width / 2, paddingtop + height / 2, radius, mpaint); } public circleview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(); } public circleview(context context) { super(context); init(); } public circleview(context context, attributeset attrs) { super(context, attrs); init(); } }
然后下面再给出一个范例,稍微复杂一点是自定义viewgroup了(主要是加强对onmeasure和onlayout的理解), 需求如下:
一个水平的viewgroup,内部的子元素 为了简单 我们假定他们的宽高都是一样的。来写一个这样的简单的viewgroup。
package com.example.administrator.motioneventtest; import android.content.context; import android.util.attributeset; import android.util.log; import android.view.view; import android.view.viewgroup; /** * created by administrator on 2016/2/4. */ //这里我们只处理了padding的状态 没有处理margin的状态,子view的margin 对measure和layout的影响 //就留给读者自己完成了 public class customhorizontallayout extends viewgroup { //设置默认的控件最小是多少 这里不提供自定义属性了 写死在代码里 你们可以自行拓展 final int minheight = 0; final int minwidth = 0; public customhorizontallayout(context context) { super(context); } public customhorizontallayout(context context, attributeset attrs) { super(context, attrs); } public customhorizontallayout(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int measurewidth = 0; int measureheight = 0; final int childcount = getchildcount(); measurechildren(widthmeasurespec, heightmeasurespec); int widthspecmode = measurespec.getmode(widthmeasurespec); int widthspecsize = measurespec.getsize(widthmeasurespec); int heightspecmode = measurespec.getmode(heightmeasurespec); int heightspecsize = measurespec.getsize(heightmeasurespec); final view childview = getchildat(0); final int paddingleft = getpaddingleft(); final int paddingright = getpaddingright(); final int paddingtop = getpaddingtop(); final int paddingbottom = getpaddingbottom(); //没有子控件 时 我们的宽高要作特殊处理 if (childcount == 0) { //当没有子控件时,如果长宽有一个为wrap 那么就让这个控件以最小的形式展现 //这里我们最小设置为0 if (widthspecmode == measurespec.at_most || heightspecmode == measurespec.at_most) { setmeasureddimension(minwidth, minheight); } else { //否则根据我们的layout属性来 setmeasureddimension(getlayoutparams().width, getlayoutparams().height); } } else if (widthspecmode == measurespec.at_most && heightspecmode == measurespec.at_most) { measurewidth = childview.getmeasuredwidth() * childcount; measureheight = childview.getmeasuredheight(); setmeasureddimension(paddingleft + measurewidth + paddingright, paddingtop + measureheight + paddingbottom); } else if (heightspecmode == measurespec.at_most) { measureheight = childview.getmeasuredheight(); setmeasureddimension(paddingleft + paddingright + widthspecsize, paddingtop + paddingbottom + measureheight); } else if (widthspecmode == measurespec.at_most) { measurewidth = childview.getmeasuredwidth() * childcount; setmeasureddimension(paddingleft + paddingright + measurewidth, paddingtop + paddingbottom + heightspecsize); } } @override protected void onlayout(boolean changed, int l, int t, int r, int b) { final int paddingleft = getpaddingleft(); final int paddingright = getpaddingright(); final int paddingtop = getpaddingtop(); final int paddingbottom = getpaddingbottom(); //左边初始位置为0 int childleft = 0 + paddingleft; final int childcount = getchildcount(); for (int i = 0; i < childcount; i++) { final view childview = getchildat(i); if (childview.getvisibility() != view.gone) { final int childwidth = childview.getmeasuredwidth(); childview.layout(childleft, 0 + paddingtop, childleft + childwidth, paddingtop + childview.getmeasuredheight()); childleft += childwidth; } } } }
以上就是本文的全部内容,希望对大家的学习有所帮助。