View的绘制流程
要成为一个优秀的Android开发者就必须要了解View的绘制流程。在DecorView添加到Window中的流程一文我们了解了DecorView添加到Window的整个过程,同时在最后留下了伏笔:View添加到Window的同时,也开启了View的绘制流程。现在继续跟着源码熟悉View的绘制流程。
1、接上回--View添加到Window
上回说到View添加到Window的流程,最后通过ViewRootImpl的setView()方法将DecorView设置给ViewRootImpl管理。该方法两百余行,其它的我们选择性忽略,只关注和View绘制主线流程相关的:
/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) { synchronized (this) { if (mView == null) { mView = view; ... // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); ... } } }
1.1、scheduleTraversals()
每当新set一个View,就会调用requestLayout()方法重新布局:将View的测量、布局、绘制重新执行一遍,以更新界面。另见《View的requestLayout()流程》一文。
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
内部调用scheduleTraversals(),借助Choreographer执行View遍历绘制流程,关于“编舞者”这里暂不展开讨论,以后有时间再安排上。
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
1.2、TraversalRunnable任务
执行的mTraversalRunnable对象是一个TraversalRunnable类型,实现Runnable接口。不用想,传给Choreographer绕来绕去,最终肯定是被Handler执行。
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
TraversalRunnable中的run()执行ViewRootImpl的doTraversal()方法。
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ... performTraversals(); ... } }
1.3、performTraversals()
performTraversals()是最终执行的方法,在这里完成了View绘制的三大流程:measure、layout、draw。该方法一共七百余行,只需关注和View绘制的核心代码段,简化版performTraversals()如下:
private void performTraversals() { ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ... // 执行测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... //执行布局 performLayout(lp, mWidth, mHeight); ... //执行绘制 performDraw(); ... }
框架中的类和方法很长,一千多行的方法都很正常,为了尽可能的提高一些性能。所以阅读源码一定要抓重点,切莫硬钻进一条逻辑走不出来。
进入View绘制流程的三个关键方法:performMeasure()、performLayout()、performDraw()。
2、measure
先开始View的测量,看看测量相关的核心代码。View的测量从performMeasure()方法开始。
2.1、performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
宽高两个参数从ViewRootImpl通过getRootMeasureSpec(int windowSize, int rootDimension)方法初始化好传过来。MeasureSpec用来将size和mode包装成一个int,主要是为了节省内存,关于MeasureSpec详见充分理解MeasureSpec。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally {} }
mView就是添加到ViewRootImpl中的DecorView,DecorView继承自FrameLayout。DecorView并没有重写measure(int widthMeasureSpec, int heightMeasureSpec),还得看父类View中的measure(int widthMeasureSpec, int heightMeasureSpec)方法:
/** * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
2.2、View中的默认onMeasure()
从上面可以看出在View的测量过程中,会调用onMeasure()回调。
/** * Measure the view and its content to determine the measured width and the * measured height. This method is invoked by {@link #measure(int, int)} and * should be overridden by subclasses to provide accurate and efficient * measurement of their contents. * * @see #getMeasuredWidth() * @see #getMeasuredHeight() * @see #setMeasuredDimension(int, int) * @see #getSuggestedMinimumHeight() * @see #getSuggestedMinimumWidth() * @see android.view.View.MeasureSpec#getMode(int) * @see android.view.View.MeasureSpec#getSize(int) */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
这个方法开发者在自定义View的时候可以重写,如果没有重写onMeasure()方法,则会默认调用getDefaultSize()来获得View的宽高,
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; }
最后调用setMeasuredDimension(int measuredWidth, int measuredHeight)方法设置当前View的测量宽高值。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
2.3、重写onMeasure():以LinearLayout和TextView为例
ViewGroup并没有重写onMeasure(),不过提供了遍历子View的测量方法:measureChildren(int widthMeasureSpec, int heightMeasureSpec),该方法已经不再被普遍使用,了解一下就行,各个容器都有自己的测量子View的方法。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
以最常见的容器FrameLayout的onMeasure()方法为例,重写的方法挨个遍历测量每个子View,统计得出宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); ... for (int i = 0; i < count; i++) { final View child = getChildAt(i); measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); ... } ... count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); ... final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) {...} else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) {...} else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } ... child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
再以TextView重写的onMeasure()方法为例,根据文字、RLT、字重等属性经过复杂的计算,最终测量得出TextView的宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; ... if (widthMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. width = widthSize; } ... if (heightMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. height = heightSize; mDesiredHeightAtMeasure = -1; } ... setMeasuredDimension(width, height); }
2.4、获取测量值
测量后,mMeasuredWidth和mMeasuredHeight这两个测量值会初始化,内部的setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法设置:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
可以通过下面两个get方法获取对应的宽高测量值mMeasuredWidth和mMeasuredHeight:
public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; } public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; }
有一个坑点,开发者应该遇到过:在Activity中如果想获取View的宽高,不管是在onCreate()还是onStart()、onResume()生命周期获取到的值可能都是0。因为View的测量和Activity的生命周期不是同步的,大概率Activity起来了,View还没有测量完毕,值未被初始化,因此获取的是0。
有没有方法在Activity中获取View的测量宽高吗?有以下三种方法:
①在Activity焦点变化的onWindowFocusChanged(hasFocus: Boolean)回调中获取View宽高。
override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight) }
②通过View的post()方法可以将一个Runnable任务放到消息队列的尾部。当UI线程完成View测量后,会执行最后的action。关于消息队列Message和Handler,详见Handler消息延迟原理。
imageViewEx = findViewById(R.id.image_exactly) imageViewEx.post { Log.d(TAG, "post measuredHeight = " + imageViewEx.measuredHeight) }
③借助ViewTreeObserver观察者,监听View变化。当View绘制完毕,回调中获取测量结果。ViewTreeObserver对绘制性能有一定影响,不建议频繁使用。
override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight) }
3、layout
measure阶段完成,进入View绘制流程第二步:layout,为了确定各View的位置。
3.1、performLayout()
接着看1.3节 performTraversals()中和layout相关的方法performLayout():
private void performLayout(WindowManager.LayoutParams lp,int desiredWindowWidth, int desiredWindowHeight) { ... final View host = mView; ... try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); } ... }
3.2、layout(int l, int t, int r, int b)
局部变量host被赋值为mView,mView我们知道是DecorView类型。调用的是DecorView的layout()方法,该方法定义在View中,内部调用了setFrame()和onLayout()方法。
/** * 为View及其所有子View分配大小和位置 * * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ public void layout(int l, int t, int r, int b) { ... 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); ... onLayout(changed, l, t, r, b); ... }
setFrame()方法传入的四个参数分别是左、上、右、下距离父View的距离,于是当前View的位置被布局好了。在两个月前写的Android坐标系一文中,有了解过这些坐标参数的含义。提前储备足够的知识,关键时候还是能串起来用的。
/** * Assign a size and position to this view. * This is called from layout. * * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent * @return true if the new size and position are different than the * previous ones */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) protected boolean setFrame(int left, int top, int right, int bottom) { ... // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); ... return changed; }
layout()方法还在ViewGroup中被重写:
@Override public final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b); } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; } }
3.3、onLayout(boolean changed, int left, int top, int right, int bottom)
View中的onLayout()方法默认为空方法:
/** * Called from layout when this view should * assign a size and position to each of its children. * * Derived classes with children should override * this method and call layout on each of * their children. * @param changed This is a new size or position for this view * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
对于ViewGroup的子类必须重写onLayout()方法,因为该方法在ViewGroup中被修改成抽象的,以便自定义的ViewGroup能够处理所有子View的布局。
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
3.4、举个例子
找个ViewGroup看看典型的onLayout()方法如何实现,以最常见的线性容器LinearLayout为例。其中的onLayout()方法根据设置的orientation方向属性选择调用layoutVertical()或者layoutHorizontal()进行垂直布局或水平布局。详细代码就不在这里展开解读了。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } }
4、draw
View的位置也已经放置好,到了View绘制流程最后一步。涉及到开发者自定义View中接触最多的一个方法:onDraw(Canvas )。
4.1、performDraw()
接着1.3节 performTraversals()中与draw相关的方法:performDraw()。
private void performDraw() { if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { return; } else if (mView == null) { return; } final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw; ... boolean canUseAsync = draw(fullRedrawNeeded); ... }
花了挺时间才找到View绘制的下一步(因为框架的代码量实在是太大,看起来有点费劲):调用ViewRootImpl中的draw(boolean )方法:
private boolean draw(boolean fullRedrawNeeded) { ... if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; } ... return useAsyncReport; }
真正绘制View的原来是ViewRootImpl中的drawSoftware(),里面调用DecorView的draw()方法完成从ViewGroup到View的挨个遍历绘制。
/** * @return true if drawing was successful, false if an error occurred */ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { // Draw with software renderer. final Canvas canvas; ... canvas = mSurface.lockCanvas(dirty); ... mView.draw(canvas); ... return true; }
4.2、View的绘制:draw(Canvas canvas)
DecorView的draw(Canvas )方法定义如下,比起View中的draw(Canvas )增加了绘制菜单背景一项。
@Override public void draw(Canvas canvas) { super.draw(canvas); if (mMenuBackground != null) { mMenuBackground.draw(canvas); } }
父类View中的draw(Canvas )方法将绘制一个View拆分为7个小步骤。其中第②步和第⑤步正常情况下都会跳过。AOSP中的源码给这段加了详细的注释,每一步骤都有说明。写代码就应该这样,注释也非常重要。
①drawBackground(Canvas canvas):绘制View的背景
②保存画布的图层,准备褪色
③onDraw(canvas):绘制View内容
④dispatchDraw(canvas):分发绘制子View
⑤绘制褪色边缘并恢复图层
⑥onDrawForeground(canvas):绘制装饰(例如滚动条)
⑦drawDefaultFocusHighlight(canvas):绘制默认的焦点突出显示
public void draw(Canvas canvas) { /* * 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) * 7. If necessary, draw the default focus highlight */ // Step 1, draw the background, if needed drawBackground(canvas); // Step 2, save the canvas' layers // Step 3, draw the content onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); // we're done... }
4.3、自定义View关键:onDraw(Canvas canvas)
自定义View其中一种方式是继承自View,通过重写onDraw(Canvas )实现个性化的界面绘制。该方法一般被子View重写,而不是ViewGroup容器子类。
/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */ protected void onDraw(Canvas canvas) { }
举个例子,以ImageView重写的onDraw(Canvas )为例,绘制我们设置的Drawable资源。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mDrawable == null) { return; // couldn't resolve the URI } if (mDrawableWidth == 0 || mDrawableHeight == 0) { return; // nothing to draw (empty bounds) } ... mDrawable.draw(canvas); ... }
4.4、绘制子View:dispatchDraw(Canvas canvas)
ViewGroup并没有实现draw()和onDraw(),那么子类递归的绘制在哪里分发的?刚刚在4.3节才看到View的draw()第④步:dispatchDraw(Canvas canvas)分发绘制子View。
/** * Called by draw to draw the child views. This may be overridden * by derived classes to gain control just before its children are drawn * (but after its own view has been drawn). * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { }
dispatchDraw(Canvas canvas)分发才是各容器分发绘制子View的核心,看看ViewGroup中重写的方法实现:
@Override protected void dispatchDraw(Canvas canvas) { ... for (int i = 0; i < childrenCount; i++) { drawChild(canvas, child, drawingTime); } ... }
遍历ViewGroup中的所有子View,调用drawChild()方法绘制子View:如果是单个View则直接绘制;如果子View还是ViewGroup容器类型,那么继续分发下去,直到所有的子View都绘制完。
/** * Draw one child of this View Group. This method is responsible for getting * the canvas in the right state. This includes clipping, translating so * that the child's scrolled origin is at 0, 0, and applying any animation * transformations. * * @param canvas The canvas on which to draw the child * @param child Who to draw * @param drawingTime The time at which draw is occurring * @return True if an invalidate() was issued */ protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
了解过View的绘制流程,开发者应该对View的绘制有更深刻的印象,对开发自定义View有一定帮助,加深View的绘制理解。关于自定义View详见以下相关的笔记及View专栏:
...