13问13答全面学习Android View绘制
本文通过13问13答学习AndroidView绘制,供大家参考,具体内容如下
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要简单的多 //代码就不分析了一目了然的东西 privatestaticintgetRootMeasureSpec(intwindowSize,introotDimension){ intmeasureSpec; switch(rootDimension){ caseViewGroup.LayoutParams.MATCH_PARENT: //Windowcan'tresize.ForcerootviewtobewindowSize. measureSpec=MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY); break; caseViewGroup.LayoutParams.WRAP_CONTENT: //Windowcanresize.Setmaxsizeforrootview. measureSpec=MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST); break; default: //Windowwantstobeanexactsize.Forcerootviewtobethatsize. measureSpec=MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY); break; } returnmeasureSpec; }
4.对于普通view来说,他的measure过程中,与父view有关吗?如果有关,这个父view也就是viewgroup扮演了什么角色?
答:看源码:
//对于普通view的measure来说是由这个view的父view,也就是viewgroup来触发的。 //也就是下面这个measureChildWithMargins方法 protectedvoidmeasureChildWithMargins(Viewchild, intparentWidthMeasureSpec,intwidthUsed, intparentHeightMeasureSpec,intheightUsed){ //第一步先取得子view的layoutParams参数值 finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams(); //然后开始计算子view的spec的值,注意这里看到计算的时候除了要用子view的layoutparams参数以外 //还用到了父view也就是viewgroup自己的spec的值 finalintchildWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin +widthUsed,lp.width); finalintchildHeightMeasureSpec=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剩余空间的大小。 publicstaticintgetChildMeasureSpec(intspec,intpadding,intchildDimension){ intspecMode=MeasureSpec.getMode(spec); intspecSize=MeasureSpec.getSize(spec); intsize=Math.max(0,specSize-padding); intresultSize=0; intresultMode=0; switch(specMode){ //Parenthasimposedanexactsizeonus caseMeasureSpec.EXACTLY: if(childDimension>=0){ resultSize=childDimension; resultMode=MeasureSpec.EXACTLY; }elseif(childDimension==LayoutParams.MATCH_PARENT){ //Childwantstobeoursize.Sobeit. resultSize=size; resultMode=MeasureSpec.EXACTLY; }elseif(childDimension==LayoutParams.WRAP_CONTENT){ //Childwantstodetermineitsownsize.Itcan'tbe //biggerthanus. resultSize=size; resultMode=MeasureSpec.AT_MOST; } break; //Parenthasimposedamaximumsizeonus caseMeasureSpec.AT_MOST: if(childDimension>=0){ //Childwantsaspecificsize...sobeit resultSize=childDimension; resultMode=MeasureSpec.EXACTLY; }elseif(childDimension==LayoutParams.MATCH_PARENT){ //Childwantstobeoursize,butoursizeisnotfixed. //Constrainchildtonotbebiggerthanus. resultSize=size; resultMode=MeasureSpec.AT_MOST; }elseif(childDimension==LayoutParams.WRAP_CONTENT){ //Childwantstodetermineitsownsize.Itcan'tbe //biggerthanus. resultSize=size; resultMode=MeasureSpec.AT_MOST; } break; //Parentaskedtoseehowbigwewanttobe caseMeasureSpec.UNSPECIFIED: if(childDimension>=0){ //Childwantsaspecificsize...lethimhaveit resultSize=childDimension; resultMode=MeasureSpec.EXACTLY; }elseif(childDimension==LayoutParams.MATCH_PARENT){ //Childwantstobeoursize...findouthowbigitshould //be resultSize=View.sUseZeroUnspecifiedMeasureSpec?0:size; resultMode=MeasureSpec.UNSPECIFIED; }elseif(childDimension==LayoutParams.WRAP_CONTENT){ //Childwantstodetermineitsownsize....findouthow //bigitshouldbe resultSize=View.sUseZeroUnspecifiedMeasureSpec?0:size; resultMode=MeasureSpec.UNSPECIFIED; } break; } returnMeasureSpec.makeMeasureSpec(resultSize,resultMode); }
5.view的meaure和onMeasure有什么关系?
答:看源码:
//view的measure是final方法我们子类无法修改的。 publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec){ booleanoptical=isLayoutModeOptical(this); if(optical!=isLayoutModeOptical(mParent)){ Insetsinsets=getOpticalInsets(); intoWidth=insets.left+insets.right; intoHeight=insets.top+insets.bottom; widthMeasureSpec=MeasureSpec.adjust(widthMeasureSpec,optical?-oWidth:oWidth); heightMeasureSpec=MeasureSpec.adjust(heightMeasureSpec,optical?-oHeight:oHeight); } //Suppresssignextensionforthelowbytes longkey=(long)widthMeasureSpec<<32|(long)heightMeasureSpec&0xffffffffL; if(mMeasureCache==null)mMeasureCache=newLongSparseLongArray(2); if((mPrivateFlags&PFLAG_FORCE_LAYOUT)==PFLAG_FORCE_LAYOUT|| widthMeasureSpec!=mOldWidthMeasureSpec|| heightMeasureSpec!=mOldHeightMeasureSpec){ //firstclearsthemeasureddimensionflag mPrivateFlags&=~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); intcacheIndex=(mPrivateFlags&PFLAG_FORCE_LAYOUT)==PFLAG_FORCE_LAYOUT?-1: mMeasureCache.indexOfKey(key); if(cacheIndex<0||sIgnoreMeasureCache){ //measureourselves,thisshouldsetthemeasureddimensionflagback onMeasure(widthMeasureSpec,heightMeasureSpec); mPrivateFlags3&=~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; }else{ longvalue=mMeasureCache.valueAt(cacheIndex); //Castingalongtointdropsthehigh32bits,nomaskneeded setMeasuredDimensionRaw((int)(value>>32),(int)value); mPrivateFlags3|=PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } //flagnotset,setMeasuredDimension()wasnotinvoked,weraise //anexceptiontowarnthedeveloper if((mPrivateFlags&PFLAG_MEASURED_DIMENSION_SET)!=PFLAG_MEASURED_DIMENSION_SET){ thrownewIllegalStateException("Viewwithid"+getId()+":" +getClass().getName()+"#onMeasure()didnotsetthe" +"measureddimensionbycalling" +"setMeasuredDimension()"); } mPrivateFlags|=PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec=widthMeasureSpec; mOldHeightMeasureSpec=heightMeasureSpec; mMeasureCache.put(key,((long)mMeasuredWidth)<<32| (long)mMeasuredHeight&0xffffffffL);//suppresssignextension } //不过可以看到的是在measure方法里调用了onMeasure方法 //所以就能知道我们在自定义view的时候一定是重写这个方法! protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ 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的测量宽高的 protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec)); } //这个方法特别简单基本可以认为就是近似的返回spec中的specSize,除非你的specMode是UNSPECIFIED //UNSPECIFIED这个一般都是系统内部测量才用的到,这种时候返回size也就是getSuggestedMinimumWidth的返回值 publicstaticintgetDefaultSize(intsize,intmeasureSpec){ intresult=size; intspecMode=MeasureSpec.getMode(measureSpec); intspecSize=MeasureSpec.getSize(measureSpec); switch(specMode){ caseMeasureSpec.UNSPECIFIED: result=size; break; caseMeasureSpec.AT_MOST: caseMeasureSpec.EXACTLY: result=specSize; break; } returnresult; } //跟view的背景相关这里不多做分析了 protectedintgetSuggestedMinimumWidth(){ 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的这个方法 publicvoidonWindowFocusChanged(booleanhasFocus){ super.onWindowFocusChanged(hasFocus); if(hasFocus){ intwidth=tv.getMeasuredWidth(); intheight=tv.getMeasuredHeight(); Log.v("burning","width=="+width); Log.v("burning","height=="+height); } }
或者重写这个方法
@Override protectedvoidonStart(){ super.onStart(); tv.post(newRunnable(){ @Override publicvoidrun(){ intwidth=tv.getMeasuredWidth(); intheight=tv.getMeasuredHeight(); } }); }
再或者:
@Override protectedvoidonStart(){ super.onStart(); ViewTreeObserverobserver=tv.getViewTreeObserver(); observer.addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener(){ @Override publicvoidonGlobalLayout(){ intwidth=tv.getMeasuredWidth(); intheight=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里。
/** *Ifthisviewdoesn'tdoanydrawingonitsown,setthisflagto *allowfurtheroptimizations.Bydefault,thisflagisnotseton *View,butcouldbesetonsomeViewsubclassessuchasViewGroup. * *Typically,ifyouoverride{@link#onDraw(android.graphics.Canvas)} *youshouldclearthisflag. * *@paramwillNotDrawwhetherornotthisViewdrawonitsown */ publicvoidsetWillNotDraw(booleanwillNotDraw){ 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范例:
packagecom.example.administrator.motioneventtest; importandroid.content.Context; importandroid.graphics.Canvas; importandroid.graphics.Color; importandroid.graphics.Paint; importandroid.util.AttributeSet; importandroid.view.View; /** *CreatedbyAdministratoron2016/2/4. */ publicclassCircleViewextendsView{ privateintmColor=Color.RED; privatePaintmPaint=newPaint(Paint.ANTI_ALIAS_FLAG); privatevoidinit(){ mPaint.setColor(mColor); } @Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ super.onMeasure(widthMeasureSpec,heightMeasureSpec); intwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec); intwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec); intheightSpecMode=MeasureSpec.getMode(heightMeasureSpec); intheightSpecSize=MeasureSpec.getSize(heightMeasureSpec); //处理为wrap_content时的情况 if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(200,200); }elseif(widthSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(200,heightSpecSize); }elseif(heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,200); } } @Override protectedvoidonDraw(Canvascanvas){ super.onDraw(canvas); //处理padding的情况 finalintpaddingLeft=getPaddingLeft(); finalintpaddingRight=getPaddingRight(); finalintpaddingTop=getPaddingTop(); finalintpaddingBottom=getPaddingBottom(); intwidth=getWidth()-paddingLeft-paddingRight; intheight=getHeight()-paddingTop-paddingBottom; intradius=Math.min(width,height)/2; canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint); } publicCircleView(Contextcontext,AttributeSetattrs,intdefStyleAttr){ super(context,attrs,defStyleAttr); init(); } publicCircleView(Contextcontext){ super(context); init(); } publicCircleView(Contextcontext,AttributeSetattrs){ super(context,attrs); init(); } }
然后下面再给出一个范例,稍微复杂一点是自定义viewgroup了(主要是加强对onMeasure和onLayout的理解),需求如下:
一个水平的viewgroup,内部的子元素为了简单我们假定他们的宽高都是一样的。来写一个这样的简单的viewgroup。
packagecom.example.administrator.motioneventtest; importandroid.content.Context; importandroid.util.AttributeSet; importandroid.util.Log; importandroid.view.View; importandroid.view.ViewGroup; /** *CreatedbyAdministratoron2016/2/4. */ //这里我们只处理了padding的状态没有处理margin的状态,子view的margin对measure和layout的影响 //就留给读者自己完成了 publicclassCustomHorizontalLayoutextendsViewGroup{ //设置默认的控件最小是多少这里不提供自定义属性了写死在代码里你们可以自行拓展 finalintminHeight=0; finalintminWidth=0; publicCustomHorizontalLayout(Contextcontext){ super(context); } publicCustomHorizontalLayout(Contextcontext,AttributeSetattrs){ super(context,attrs); } publicCustomHorizontalLayout(Contextcontext,AttributeSetattrs,intdefStyleAttr){ super(context,attrs,defStyleAttr); } @Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ super.onMeasure(widthMeasureSpec,heightMeasureSpec); intmeasureWidth=0; intmeasureHeight=0; finalintchildCount=getChildCount(); measureChildren(widthMeasureSpec,heightMeasureSpec); intwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec); intwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec); intheightSpecMode=MeasureSpec.getMode(heightMeasureSpec); intheightSpecSize=MeasureSpec.getSize(heightMeasureSpec); finalViewchildView=getChildAt(0); finalintpaddingLeft=getPaddingLeft(); finalintpaddingRight=getPaddingRight(); finalintpaddingTop=getPaddingTop(); finalintpaddingBottom=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); } }elseif(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ measureWidth=childView.getMeasuredWidth()*childCount; measureHeight=childView.getMeasuredHeight(); setMeasuredDimension(paddingLeft+measureWidth+paddingRight,paddingTop+measureHeight+paddingBottom); }elseif(heightSpecMode==MeasureSpec.AT_MOST){ measureHeight=childView.getMeasuredHeight(); setMeasuredDimension(paddingLeft+paddingRight+widthSpecSize,paddingTop+paddingBottom+measureHeight); }elseif(widthSpecMode==MeasureSpec.AT_MOST){ measureWidth=childView.getMeasuredWidth()*childCount; setMeasuredDimension(paddingLeft+paddingRight+measureWidth,paddingTop+paddingBottom+heightSpecSize); } } @Override protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){ finalintpaddingLeft=getPaddingLeft(); finalintpaddingRight=getPaddingRight(); finalintpaddingTop=getPaddingTop(); finalintpaddingBottom=getPaddingBottom(); //左边初始位置为0 intchildLeft=0+paddingLeft; finalintchildCount=getChildCount(); for(inti=0;i<childCount;i++){ finalViewchildView=getChildAt(i); if(childView.getVisibility()!=View.GONE){ finalintchildWidth=childView.getMeasuredWidth(); childView.layout(childLeft,0+paddingTop,childLeft+childWidth,paddingTop+childView.getMeasuredHeight()); childLeft+=childWidth; } } } }
以上就是本文的全部内容,希望对大家的学习有所帮助。