引言
上期View的绘制流程(二):从Activity的生命周期到显示页面讲到在WindowManagerGlobal.addView()调用了ViewRootImpl.setView()方法,然后在ViewRootImpl的setView()方法中调用requestLayout(),然后调用performTraversals()进入View的测量、布局、绘制流程。这期将从performTraversals(),详细分析performTraversals()中的三个重要的方法 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec) 、performLayout(lp, mWidth, mHeight) 、performDraw()。
所以,一个完整的绘制流程包括measure、layout、draw三个步骤,其中:
measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来
layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置
draw:绘制。确定好位置后,就将这些控件绘制到屏幕上
每个View负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作
Measure — 测量
MeasureSpec
从上面的源码我们可以发现,系统会用一个int型的值(childWidthMeasureSpec和childHeightMeasureSpec)来存储View的宽高的信息
上文中用于生成childWidthMeasureSpec和childHeightMeasureSpec的getRootMeasureSpec()方法
这个方法实际上调用的就是MeasureSpec.makeMeasureSpec()
那么这个MeasureSpec到底是什么意思呢?MeasureSpec概括了从父布局传递给子view的布局要求,包括了测量模式和测量大小。分析代码可知,int长度为32位,高2位表示mode(模式),后30位用于表示size(大小)
有三种mode:
UNSPECIFIED:不对View大小做限制,如:ListView,ScrollView
EXACTLY:确切的大小,如:100dp或者march_parent
AT_MOST:大小不可超过某数值,如:wrap_content
子View的LayoutParams / 父view的MeasureSpecEXACTLYAT_MOSTUNSPECIFIED
具体大小(如100dp)EXACTLYEXACTLYEXACTLY
match_parentEXACTLYAT_MOSTUNSPECIFIED
wrap_contentAT_MOSTAT_MOSTUNSPECIFIED
当View采用固定宽高时(即设置固定的dp/px),不管父容器是什么模式,View都是EXACTLY模式,并且大小遵循我们设置的值
当View的宽高是match_parent时,如果父容器的是EXACTLY模式,那么View也是EXACTLY模式且其大小是父容器的剩余空间;如果父容器是AT_MOST模式那么View也是AT_MOST模式并且其大小不会超过父容器的剩余空间
当View的宽高是wrap_content时,View都是AT_MOST模式并且其大小不能超过父容器的剩余空间
只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小
onMeasure
mView.measure()内部会调用onMeasure()
一个View的实际测量工作是在onMeasure()中实现的,onMeasure()已经默认为我们的控件测量了宽高
在自定义ViewGroup时,默认的onMeasure()往往不能满足我们的需求,这时候就要重写该方法,在该方法内测量子View的尺寸。当重写onMeasure()时,必须调用setMeasuredDimension(width,height)来存储该View测量出的宽和高。如果不这样做将会触发IllegalStateException
ViewGroup提供了三个方法测量子View的宽高
measureChildren(intwidthMeasureSpec,intheightMeasureSpec)
measureChild(Viewchild,intparentWidthMeasureSpec)
protectedvoidmeasureChildWithMargins(Viewchild,..)
View和ViewGroup重写onMeasure的差异
下面用两个例子分别来展示一下View和ViewGroup重写onMeasure的差异
View
View一般只关心自身尺寸的测量
ViewGroup
ViewGroup一般会先遍历子View,调用子View的测量方法,然后在再结合子View的尺寸来确定自身的大小
Layout - 布局
前面measure的作用是测量每个View的尺寸,而layout的作用是根据前面测量的尺寸以及设置的其它属性值,共同来确定View的位置
ViewGroup的layout()
从源码中可以看出实际上调用的还是View的layout()方法
View的layout()
从源码可以看出layout()最终通过setFrame()方法对view的四个属性(mLeft、mTop、mRight、mBottom)进行了赋值,从而去确定View的大小和位置,如果发生改变则调用onLayout()方法
onLayout
我们先来看看ViewGroup的onLayout()方法,该方法是一个抽象方法。因为layout过程是父布局容器布局子View的过程,onLayout()方法对子View没有意义,只有ViewGroup才有用,所以ViewGroup应该重写该方法并为每一个子View调用layout()
protectedabstractvoidonLayout(booleanchanged,intl,intt,intr,intb);
我们再来看看顶层ViewGroup,也就是DecorView的onLayout()方法。DecerView继承自FrameLayout,所以我们直接看FrameLayout的onLayout()方法
@OverrideprotectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){layoutChildren(left,top,right,bottom,false/* no force left gravity */);}voidlayoutChildren(intleft,inttop,intright,intbottom,booleanforceLeftGravity){finalintcount=getChildCount();......for(inti=0;i<count;i++){finalViewchild=getChildAt(i);if(child.getVisibility()!=GONE){finalLayoutParamslp=(LayoutParams)child.getLayoutParams();finalintwidth=child.getMeasuredWidth();finalintheight=child.getMeasuredHeight();......child.layout(childLeft,childTop,childLeft+width,childTop+height);}}
我们可以看到,这里面会对每一个child调用layout()方法。如果该child仍然是ViewGroup,会继续递归下去;如果是叶子View,则会走到View的onLayout空方法,该叶子View布局流程就走完了。另外,width和height分别来源于measure阶段存储的测量值
Draw - 绘制
当layout完成后,就进入到draw阶段了,在这个阶段,会根据layout中确定的各个view的位置将它们画出来
前面说过,mView就是DecorView,所以我们直接来看DecorView的draw()方法
@Overridepublicvoiddraw(Canvascanvas){super.draw(canvas);if(mMenuBackground!=null){mMenuBackground.draw(canvas);}}
调用完super.draw()后,还画了菜单背景。我们继续关注super.draw()方法,会发现FrameLayout和ViewGroup都没有重写该方法,直接进到了View的draw()方法
@CallSuperpublicvoiddraw(Canvascanvas){......intsaveCount;// Step 1, draw the background, if neededif(!dirtyOpaque){drawBackground(canvas);}// Step 2, If necessary, save the canvas' layers to prepare for fading......// Step 3, draw the contentif(!dirtyOpaque)onDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);//Step 5, If necessary, draw the fading edges and restore layers......// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);45......}
主要是就是如下几步,其中最重要的就是画内容和画子View
画背景。对应我我们在xml布局文件中设置的android:background属性
画内容。通过重写onDraw()方法
画子View。dispatchDraw()方法用于帮助ViewGroup来递归画它的子View
画装饰。这里指画滚动条和前景。其实平时的每一个View都有滚动条,只是没有显示而已
onDraw()
当自定义View需要进行绘制的时候,我们往往会重写onDraw()方法,这里放一个简单的例子感受一下
PaintmPaint=newPaint(Paint.ANTI_ALIAS_FLAG);@OverrideprotectedvoidonDraw(Canvascanvas){mPaint.setColor(Color.YELLOW);canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);mPaint.setColor(Color.BLUE);mPaint.setTextSize(20);Stringtext="Hello View";canvas.drawText(text,0,getHeight()/2,mPaint);}