谈谈 View 绘制流程
注:本文使用 sdk 23 作为源码参考。
- 前言
- ViewRootImpl#performTraversals()
- ViewRootImpl#performMeasure()
- ViewRootImpl#performLayout()
- ViewRootImpl#performDraw()
- some tips
- onDraw()
- dispatchDraw()
前言
关于 View 的绘制流程,网上铺天盖地的文章已经都把这个机制说烂了,笔者撰写此文一面为了方面自己后期回顾,一面也试着使用更通俗一点的方式来阐述这个机制。
ViewRootImpl#performTraversals()
众所周知, ViewRootImpl#performTraversals()
是触发 View 绘制流程的起始点,而在 ViewRootImpl#performTraversals()
中会触发相应的 ViewRootImpl#performMeasure()
、ViewRootImpl#performLayout()
、ViewRootImpl#performDraw()
对应着测量
、布局
、绘制
三个过程。(不得不说函数、变量的命名对源码的理解还是有很大的帮助的,从 ViewRootImpl#performTraversal()
这个函数名就应该能猜到这个函数是完成遍历的过程,完成什么的遍历?完成测量
的遍历、布局
的遍历、绘制
的遍历),为了加深各位读者的理解,笔者将在源码解析的过程中,一步一步绘制流程图,比起文章尾部丢上一张完整的流程图,笔者认为这样的做法会更加友善——
ViewRootImpl#performMeasure()
首先是测量过程,删除无关代码后简化代码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
可以看到,实际上在 ViewRootImpl#performMeasure()
中调用了 View 的 measure()
方法,打开 View#measure()
方法看一看,可以看到这个方法是 fianl
的,所以对于它的子类来说是不能够覆写该方法的,而在其内部调用了 View#onMeasure()
方法,那么接下来就该看看 onMeasure()
方法的实现了,在 View 中 onMeasure()
方法实现如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
setMeasuredDimension(measuredWidth, measuredHeight)
方法用官方文档的意思就是用来保存测量后的宽高。
那么 ViewGroup 中的实现呢?实际上在源码中可以看到,ViewGroup 并没有按照常理将其设置为 abstract 类型,但是 onMeasure()
上方的注释文档提到:子类需要重写它才能够获取到精确、有效的测量值
,所以对于 View 的子类来说,都应该去重写该方法,所以实际上不仅仅是针对于 ViewGroup 来说,对于 TextView、ImageView 来说也是需要重写 onMeasure()
方法的。对于 View 的 onMeasure()
方法本文就不加以扩展了,毕竟测量
这种操作对于纯粹的 View 来说就是测量自己的大小,所以不妨着重看看 ViewGroup 的实现,以 LinearLayout 举例:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}
LinearLayout 根据 orientation 的设置来选择测量方式,笔者这里选择 LinearLayout#measureVertical()
来阐述,LinearLayout#measureHorizontal()
意义等同。LinearLayout#measureVertical()
源码删减后如下:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { // ... } else { // ... measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0); } } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);}
for 循环是对子 View 的遍历。if 判断语句的执行条件中有一点是 lp 的高度是0,也就意味着子 View 的 layout_height 设置为了 0,这种情况就不需要对子 View 进行 measure 操作了,因为对于竖直 LinearLayout 来说,它更关注于子 View 的高度。这也就是意味着正常情况下都是会走 else 分支,也就是说正常情况下每个 view 都会被执行 LinearLayout#measureChildBeforeLayout()
方法,跟踪 LinearLayout#measureChildBeforeLayout()
方法可以发现实际上底层是调用了 View 的 measure()
方法——
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { // ... child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
所以实际上对于 ViewGroup 来说,它的 onMeasure()
实际上就是调用各个子 View 的 measure()
方法来将它们的某些值做一些汇总然后拼凑成自己的宽高。
[外链图片转存中…(img-DDJjZO65-1590468520904)]
实际上上述源码中的
measureChildWithMargins()
只是 ViewGroup 提供的一种测量子 View 的函数,与此类似的还有ViewGroup#measureChild()
。除了测量单个子 View 的函数外,ViewGroup 还有提供measureChildren()
这种子 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); } }}
但是实际上该函数在 ViewGroup 子类实现中运用的不多,像 LinearLayout/FrameLayout/RelativeLayout 中都是自行实现子 View 遍历,底层只会调用
measureChild()
或者measureChildWithMargins()
。注:笔者见部分书籍和博客常谈到
measureChildren()
这个函数,且甚至有博客笼统地称 ViewGroup 均会调用该函数来遍历测量子 View,故特此撰写 tip。且笔者认为从使用率上来说,该函数在 ViewGroup 中运用地太少,意义并不是很大。
ViewRootImpl#performLayout()
ViewRootImpl#performLayout()
其实与 ViewRootImpl#performMeasure()
类似,底层实现通过调用 View#layout()
来实现的——
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { // ... host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); // ...}
所以不妨打开 layout()
方法看一看——
public void layout(int l, int t, int r, int b) { setFrame(l, t, r, b); onLayout(changed, l, t, r, b);}
可以看到,实际上 View#layout()
做了两步,一步是调用 setFrame()
设置自身上下左右四个顶点的位置,这样自身的位置就已经布好了,第二步是 onLayout()
——
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
View 中 onLayout()
方法竟然是一个空实现,不仅如此,ViewGroup 作为抽象类,onLayout()
是它唯一声明的抽象方法,可见该方法对于 ViewGroup 来说有多重要了,它的官方注释说到:当 View 需要给它的孩子设置大小和位置的时候应该被调用
。所以从这里可以看出,对于纯粹的 View 来说,onLayout()
的意义可能不是很大(从源码看来,View 中只有 TextView 对其稍有扩展),但是对于 ViewGroup 来说确是至关重要的一个函数。毕竟作为一个 ViewGroup 来说,多样性的体现就在于对子 View 的摆放。
同样的,拿 LinearLayout 来举例,看看 LinearLayout 的 onLayout()
源码实现——
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); }}
同样的,我们不妨选择参考 layoutVertical()
的实现——
void layoutVertical(int left, int top, int right, int bottom) { for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); }}
内部会对子 View 进行遍历,并调用 setChildFrame()
,而 setChildFrame()
的底层实现也就是 View#layout()
——
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height);}
所以,对于 ViewGroup 来说,它的 layout()
方法先会对自己进行定位(setFrame()
),再遍历调用子 View 的 layout()
(/setFrame()
) 将所有的子 View 进行布局。
[外链图片转存中…(img-6VjtBkFI-1590468520906)]
ViewRootImpl#performDraw()
等同于 performMeasure()
、performLayout()
,ViewRootImpl#performDraw()
底层会调用 draw()
方法——
private void performDraw() { // ... draw(); // ...}
当然,这里可以注意到这个 draw()
方法是位于 ViewRootImpl 中而不是 View 中的,打开 draw()
并删除无关代码:
private void draw(boolean fullRedrawNeeded) { if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; }}
继续打开 drawSoftwar()
方法:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { mView.draw(canvas);}
所以最终 ViewRootImpl#draw()
最终底层还是通过 View#draw()
来实现的。
来看看 View 的 draw()
方法的实现:
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) */ // Step 1, draw the background, if needed int 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 content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; }}
源码注释已经说得很清楚了:
1.绘制背景
2.如果有需要的话,保存图层,为渐变做准备
3.绘制自身
4.绘制子 View
5.如果有需要的话,绘制渐变并重新存储图层
6.绘制装饰
从上方的源码也可以看出,大部分2、5点是可以跳过的,所以日常开发中需要注意到1、3、4、6的执行顺序就好了。而从这里可以看出两点,其一对于 ViewGroup 来说,要关注到第4点也就是 dispatchDraw()
的实现了,实际上对于 ViewGroup 来说,它的实现也等同于测量和布局,也就是循环遍历调用子类的 draw()
方法;其二对于 View 来说,需要关注到第3点也就是 onDraw()
的实现了,实际上在日常开发自定义 View 中 onDraw()
方法应该是最常见覆写的 API 了,所以笔者再此也不做扩展了。
[外链图片转存中…(img-GntcTw5O-1590468520907)]
some tips
onDraw()
在本文中并未涉及,笔者照搬《Android 开发艺术探索》上的内容了——如果是继承自 View,需要自行支持 wrap_content,且 padding 也需要自行处理。
这里需要加粗的地方就是继承自 View,如果是 TextView 等原生控件可以不考虑处理 wrap_content 和 padding,因为原生控件已经覆写了 onDraw()
方法。
dispatchDraw()
具有一定的经验的读者知道,对于 ViewGroup 来说,大部分情况下 draw(Canvas canvas)
方法是不会被调用,但是 dispatchDraw()
方法在正常情况下都是会被调用的。缘由在 View 中的 updateDisplayListIfDirty()
中有这么一段:
// Fast path for layouts with no backgroundsif ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas);} else { draw(canvas);}
可以看到会对一些 flag 进行判断,如果判断结果符合则执行 dispatchDraw()
方法,否则才执行 draw()
方法。那么这个 flag 如何设置呢?通过 View#setWillNotDraw(boolean)
就可以设置了,对于 View 来说是关闭这个 flag 的,而对于 ViewGroup 来说是默认开启这个 flag 的,当开发者可以手动开启或者调用部分绘制 API(如 drawBackground()
)的时候才会关闭这个 flag。那么这样做的目的是什么呢——对于 ViewGroup 来说,它存在的意义主要在于测量和布局的过程,而视图『具体效果』的展示其实都在子 View 身上,ViewGroup 的重点并不在此。
各位读者可以结合实际情况想想是不是这么一回事,理解了这个概念就不再会困惑于为什么 ViewGroup 的 draw()/onDraw()
方法不一定会被调用了。所以对于自定义 ViewGroup 来说,在有相应的需求下尽量去覆写 dispatchDraw()
而不是 onDraw()
方法,但是事实上针对一般的 ViewGroup 来说也不需要去覆写该方法,沿用 ViewGruop 的即可(源码中 LinearLayout、FrameLayout、RelativeLayout 等常见布局沿用 ViewGroup 的 dispatchDraw()
方法)。
更多相关文章
- android很的意思的事情,关于Input…
- Android(安卓)面试整理
- Ubuntu 编译Android若干错误及解决方法(转)
- Android(安卓)MediaPlayer基本使用方式
- [Android] IntentInjector
- Android(安卓)读取内存文件返回byte数组
- Android(安卓)sqlite cursor的遍历
- Android工程导入时常见的错误解决方法
- Android(安卓)-- 倒计时的实现