View的工作原理
初始ViewRoot和DecorView
ViewRoot对应的实体类是ViewRootImpl类,它时连接WindowManager和DecorView的纽带
View的三大流程都是通过ViewRoot完成的
在ActivityThread中,当Activity对象被创建后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewImpl对象和DecorView建立关联:
|
|
View的绘制流程是从ViewRoot的performTraversals中开始的,它经过measure,layout,draw三个过程。
performTraversals大致流程:
childLayoutParams\parentSpecMode | ECACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY/childSize | EXACTLY/childSize | EXACTLY/childSize |
match_parent | EXACTLY/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
wrap_content | AT_MOST/parentSize | AT_MOST/parentSize | UNSPECIFIED/0 |
DecorView作为顶级View,本质是一个LinearLayout,该layout中一般情况下包含标题栏和内容栏。
在Activity中,我们setContentView所设置的布局,其实就是被加到内容栏中的
理解MeasureSpec
MeasureSpec很大程度上决定了一个View的尺寸规格,之所以说很大程度,其实时因为在这个过程中还受父容器的影响,父容器回影响View的MeasureSpec的创建。
MeasureSpec代表一个32位的int值,高2位代表SpecMode:测量模式,低30位表示SpecSize:在某种模式下的测量规格。
在下面的源码中,MeasureSpec将SpecMode和SpecSize打包成一个int值来避免过多对象内存的分配。为了方便操作提供了打包和解包的方法
|
|
SpecMode有三类:
UNSPECIFIED:父容器不对View有限制,这种情况一般用于系统内部,表示一种测量的状态
EXACTLY:父容器已经检验出View所需要的大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式
AT_MOST:父容器指定了一个可用的大小,即SpecSize,View的大小不能大于这个值。它对应于LayoutParams中的wrap_content
MeasureSpec和LayoutParams的对应关系
正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转为对应的MeasureSpec,然后再根据这个MeasureSpec来确定View的宽高
LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高
对于顶级View(DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定
DecorView的MeasureSpec创建过程是在ViewRootImpl中的measureHierarchy方法创建的:
123childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着看看getRootMeasureSpec:
1234567891011121314151617<pre style="background-color:#21282d;color:#e0e2e4;font-family:'Courier New';font-size:15.0pt;">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;</pre>
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
固定大小:精确模式,大小为LayoutParams中指定的大小。
- 对于普通View来说(指的时我们布局中的View),它的measure过程是由ViewGroup传递过来的,下面看看ViewGroup中的measureChildWithMargins:
|
|
- 从上面代码来看,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还有与View的margin和padding有关,下面再看看ViewGroup的getChildMeasureSpec:
|
|
从上面的方法来看,它的主要作用时结合父容器发MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding指的是父容器中已被占用的空间大小。
下面的表是对getChildMeasureSpec的工作原理的梳理,其中parentSize指的是父容器中目前可以使用的大小。
View的工作过程
View的measure过程:
- measure方法时一个final方法,则意味着不能重写该方法,在该方法中会调用View的onMeasure方法(真正测量的方法), 下面我们看看onMeasure的实现:
|
|
setMeasureDimension方法中会设置View的宽高的测量值,因此,我们来看看getDefaultSize方法:
|
|
对于我我们来说,我们只看AT_MOST和EXACTLY这两种情况。简单的理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,这个specSize就是测量后View的大小,这里说时是测量后,是因为View的最终大小是在layout阶段确定的,除了特殊情况外,View的测量大小和最终大小是相等的。
- 对于UNSPECIFIED来说,一般用于系统内部测量,在这种情况下,View的大小为getDefaultSize第一个参数size,即分别为getSuggestedMinimumWidth/Height:
|
|
如果View没有设置背景,那么View的高度为mMinHeight,而该属性对应于aandroid:minWidth,如果这个属性不指定的话,默认为0。如果设置了背景则返回minHeight和背景的最小高度中的最大值
从getDefaultSize的实现来看,我们可以得到以下小结:
直接继承View的自定义控件需要重写onMeasure方法设置wrap_content时的自身大小,否则在布局中使用就相当于match_parent
通过之前分析,如果View在布局中被设为wrap_content的话,它的specMode时AT_MOST模式,此时它的宽高等于specSize,而View的specSize是parentSize,parentSize是父容器目前所剩余的空间大小。这样这种效果就相当于match_parent
解决方法:
1234567891011121314protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthSpecMode = View.MeasureSpec.getMode((widthMeasureSpec));int widthSpecSize = View.MeasureSpec.getMode((widthMeasureSpec));int heightSpecMode = View.MeasureSpec.getMode((heightMeasureSpec));int heightSpecSize = View.MeasureSpec.getMode((heightMeasureSpec));if(widthSpecMode == View.MeasureSpec.AT_MOST && heightSpecMode == View.MeasureSpec.AT_MOST) {setMeasureDimension(mWidth, mHeight);} else if(widthSpecMode == View.MeasureSpec.AT_MOST) {setMeasureDimension(mWidth, heightSpecSize);} else if(heightSpecMode == View.MeasureSpec.AT_MOST){setMeasureDimension(widthSpecSize, mHeight);}}
对于wrap_content的情形,我们直接设置我们默认的大小就可以
ViewGroup的measure过程:
ViewGroup除了测量自身的大小外,还会遍历所有的子元素的measure方法,各个子元素再递归去执行这个过程
ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是提供了一个measureChildren的方法:
|
|
从上面的代码来看,ViewGroup在meaure的时候,会对每个子元素进行measure,下面来看看measureChild:
|
|
meaureChild中,就是取出子元素的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec。将MeaureSpec直接传递给View的meaure进行测量。
我们知道ViewGroup是一个抽象类,并没有实现具体的测量过程,所以,我们来看看它子类中的实现,下面用LinearLayout来当例子
首先先来看看其onMeasure方法:
|
|
上面代码中,根据不同的布局方向来测量,我们选择来看竖直方向的测量过程中的一段代码:
|
|
系统会遍历每个子元素,并且对每个子元素进行measureChildBeforeLayout,这个方法会调用子元素的measure,这样就开始了measure过程,并且系统会通过mToatalLength这个变量来保存LinearLayout在竖直方向上的初步高度,当测量完子元素时,LinearLauout会测量自己的大小:
|
|
分析到这里算是把measure分析完了,下面我们来看看需要注意的一些地方:
- 如果要获取View的测量宽高或者最终宽高的话,比较好的做法是在onLayout中获取,因为在某些极端的情况下,measure可能会被多次调用,这样在onMeasure获取的宽高就不准确
四种方法中的三种解决Activity启动时获取View的宽高
在onWindowFocusChanged中获取: 该方法会在窗口的得到和失去焦点的时候被调用。代码如下:
1234567public void onWindowFocusChanged(boolean hasFocus) {super.onWindowFocusChanged(hasFocus);if(hasFocus) {int width = view.getMeasureWidth();int heigh = view.getMeasureHeight();}}view.post(runnable):将一个runnable投递到消息队列尾部,等待Looper调用此runnable的时候,View已经初始化完成了。代码如下:
1234567891011protected void onStart() {super.onStart();view.post(new Runnable() {public void run() {int width = view.getMeasureWidth();int height = view.getMeasureHeight();}});}ViewTreeObserver:使用其中的OnGlobalLayoutListener这个接口,当View状态或者View树内部的View可见性发现改变时,onGlobalLayout会被回调。代码如下:
12345678910111213protected void onStart() {super.onStart();ViewTreeObserver observer = view.getViewTreeObserver();observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {public void onGlobalLayout() {view.getViewTreeObserver().removeGlobalOnLayoutListener(this);int width = view.getMeasureWidth();int height = view.getMeasureHeight();}});}
layout过程
- 先看看View的layout方法:1234567891011121314151617181920212223242526272829303132public 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);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;}
在layout中首先会通过setFrame方法来设定View的四个顶点位置,即初始化mLeft, mRight, mTop, mBottom,view的顶点确定后,那么View在ViewGroup中的位置也相应的确定;接着调用onLayout的方法,该方法的具体实现是在ViewGroup的子类中,我们来看看LinearLayout中是怎么实现的:
- 我们只拿竖直方向上的来分析:
|
|
此方法会遍历所有子元素并调用setChildFrame来为子元素指定相应的位置,该方法中调用了子元素的layout方法,这样就实现了View的一层层测量。
分析完了layout,我们来看看哪些需要注意的地方:
- View的getMeasureWidth和getWidth有什么区别:我们首先来看看getWidth的实现:1234"layout").ExportedProperty(category =public final int getWidth() {return mRight - mLeft;}
在View的默认实现过程中,View的测量宽高和最终宽高时相等的,只不过两者的赋值时机不同而已,测量宽高是在measure,而最终宽高是在layout。
- View的getMeasureWidth和getWidth有什么区别:我们首先来看看getWidth的实现:
draw过程:
View的绘制过程遵循下面这几步:
- 绘制自己的背景(bakcground.draw(canvas))
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
上面的这个步骤可以在draw源码中看出来:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647public 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 neededint 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 contentif (!dirtyOpaque) onDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);// Overlay is part of the content and draws beneath Foregroundif (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);// we're done...return;}
View的绘制过程的传递时通过dispatchDraw来实现的,该方法回遍历所有子元素的draw(具体的实现实在ViewGroup的子类中),这样draw就可以一层层传递下去。View中的一个特殊方法:setWillNotDraw:
如果一个View不需要绘制任何内容的话,我们就可以把这个标记设为true。默认情况下View没有启动这个标志,但是ViewGroup会默认启动。如果我们自定义控件的时候需要继承ViewGroup时,我们就可以开启这个标记来便于系统的后续优化。
以上的笔记来源于安卓开发艺术探索