目录
作用
根据 measure 测量出的宽高,layout 布局的位置,渲染整个 View 树,将界面呈现出来。
具体分析
以下源码基于版本27
DecorView 的draw 流程
在《View的绘制-measure流程详解》中说过,View 的绘制流程是从 ViewRootViewImpl 中的 performMeasure()
、performLayout
、performDraw
开始的。在执行完 performMeasure()
、performLayout
后,开始执行 performDraw
方法:(以下源码有所删减)
//ViewRootViewImpl 类
private void performDraw() {
....
draw(fullRedrawNeeded);
....
}
-------------------------------------------------------------------------
//ViewRootViewImpl 类
private void draw(boolean fullRedrawNeeded) {
....
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
....
}
-------------------------------------------------------------------------
//ThreadedRenderer 类
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
....
updateRootDisplayList(view, callbacks);
....
}
-------------------------------------------------------------------------
//ThreadedRenderer 类
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
....
updateViewTreeDisplayList(view);
....
}
-------------------------------------------------------------------------
//ThreadedRenderer 类
private void updateViewTreeDisplayList(View view) {
view.mPrivateFlags |= View.PFLAG_DRAWN;
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
== View.PFLAG_INVALIDATED;
view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
//这里调用了 View 的 updateDisplayListIfDirty 方法
//这个 View 其实就是 DecorView
view.updateDisplayListIfDirty();
view.mRecreateDisplayList = false;
}
接下来查看 View 的 updateDisplayListIfDirty 方法:
代码语言:javascript复制//View 类
public RenderNode updateDisplayListIfDirty() {
....
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
/*最终调用了 DecorView 的 draw 方法,为什么没有走上面的 dispatchDraw(canvas)
我也母鸡啊,我是Debug 断点调试晓得走这里的,哈哈*/
draw(canvas);
}
....
}
-------------------------------------------------------------------------
//DecorView 重写了 draw 方法。所以走到了 DecorView 的 draw 方法
@Override
public void draw(Canvas canvas) {
//调用父类 (View)的 draw 方法
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
View 的 draw 流程
就这样, View 的绘制就开始啦。主要有四个步骤:
-
drawBackground
绘制背景色 -
onDraw
绘制内容 -
dispatchDraw
绘制 children -
onDrawForeground
绘制装饰(前景,滚动条)
//View 类
/**
*手动将此视图(及其所有子项)渲染到给定的Canvas。在调用此函数前,视图必须已经完成了完整布局(layout)。
*一般我们在自定义控件继承 View 的时候,不要重写 draw 方法,只需重写 onDraw 方法
*/
public void draw(Canvas canvas) {
....
// 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
//绘制 children
dispatchDraw(canvas);
drawAutofilledHighlight(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);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
....
}
我们对四个步骤进行分析:
代码语言:javascript复制//View 类
//绘制背景
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
//如果没有设置背景,就不进行绘制
if (background == null) {
return;
}
//如果设置了背景吗,且背景的大小发生了改变,
//就用 layout 计算出的四个边界值来确定背景的边界
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mThreadedRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
//调用 Drawable 的 draw 方法来进行背景的绘制
background.draw(canvas);
} else {
//平移画布
canvas.translate(scrollX, scrollY);
//调用 Drawable 的 draw 方法来进行背景的绘制
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
-------------------------------------------------------------------------
//View 类
//绘制内容
protected void onDraw(Canvas canvas) {
/*View 中的 onDraw 是一个空实现。也不难理解,当我们自定义控件继承 View
的时候,需要重写 onDraw 方法,通过 Canvas 和 Paint 来进行内容的绘制*/
}
-------------------------------------------------------------------------
//View 类
//绘制 children
protected void dispatchDraw(Canvas canvas) {
/*View 中的 dispatchDraw 也是一个空实现。因为单独一个 View
本身是没有子元素的,不需要绘制 children */
}
-------------------------------------------------------------------------
//View 类
//绘制装饰
public void onDrawForeground(Canvas canvas) {
//绘制指示器
onDrawScrollIndicators(canvas);
//绘制滚动条
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
//调用 Drawable 的 draw 方法,绘制前景色
foreground.draw(canvas);
}
}
以上就是 View 的绘制流程了。ViewGroup 本身是继承 View 的,它的基本绘制流程也是通过父类 View 进行的,只不过它重写了 dispatchDraw 方法,来进行子元素的绘制。下面我们来进行具体分析:
ViewGroup 的绘制 dispatchDraw 流程
代码语言:javascript复制//ViewGroup 类
@Override
protected void dispatchDraw(Canvas canvas) {
....
for (int i = 0; i < childrenCount; i ) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex ;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
//调用 drawChild 方法,进行绘制子元素
more |= drawChild(canvas, child, drawingTime);
}
}
....
}
-------------------------------------------------------------------------
//ViewGroup 类
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
//调用 View 的 draw 方法,这里要注意,调用的是 View 的三个参数的 draw 方法
return child.draw(canvas, this, drawingTime);
}
在 View 中还有一个 draw(Canvas canvas)
的重载方法,就是 draw(Canvas canvas, ViewGroup parent, long drawingTime)
:
//View 类
/**
* ViewGroup.drawChild()调用此方法以使每个子视图自己绘制。
* 这是View专门根据图层类型和硬件加速来渲染行为的地方。
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
....
//是否支持硬件加速
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
if (layerType != LAYER_TYPE_NONE) {
//未开启
//调用 View 的 buildDrawingCache 方法
buildDrawingCache(true);
}
cache = getDrawingCache(true);
}
//开启了硬件加速
if (drawingWithRenderNode) {
//调用 View 的 updateDisplayListIfDirty 方法
renderNode = updateDisplayListIfDirty();
if (!renderNode.isValid()) {
// Uncommon, but possible. If a view is removed from the hierarchy during the call
// to getDisplayList(), the display list will be marked invalid and we should not
// try to use it again.
renderNode = null;
drawingWithRenderNode = false;
}
}
....
}
分别查看 buildDrawingCache
和 updateDisplayListIfDirty
方法:
//View 类
public void buildDrawingCache(boolean autoScale) {
....
buildDrawingCacheImpl(autoScale);
....
}
-------------------------------------------------------------------------
//View 类
private void buildDrawingCacheImpl(boolean autoScale) {
....
// 如果不需要进行自身绘制,就直接调用 dispatchDraw 绘制子 Children
//否则就直接调用 View 的 draw 方法
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
draw(canvas);
}
....
}
-------------------------------------------------------------------------
//View 类
public RenderNode updateDisplayListIfDirty() {
....
// 如果不需要进行自身绘制,就直接调用 dispatchDraw 绘制子 Children
//否则就直接调用 View 的 draw 方法
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
draw(canvas);
}
....
}
如此,从顶层 DecorView 的 draw 方法开始,然后调用 dispatchDraw 方法循环遍历绘制子元素,如果子元素是继承了 ViewGroup ,就再次循环调用 dispatchDraw 方法,一层层往下递归调用,直到每一个子元素都被绘制完成,整个 draw 流程也就结束了。
tips: 在我们使用真机进行源码断点调试的时候,可能会出现源码不能打断点的情况,或者断点没有走在该走的地方。这是因为国内手机厂商基本都是定制系统,可能修改了源码。这个时候可以使用模拟器进行断点调试。注意:模拟器版本号要和项目编译版本号一致!
setWillNotDraw 解析
在 View 中有一个方法是 setWillNotDraw:
代码语言:javascript复制//View 类
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从注释上看,如果此视图本身不执行任何绘制,就设置为 true,系统会进行一些绘制优化。View 本身是默认设置为 false 的,没有启动这个优化标记(这也不难理解,因为一般我们自定义控件继承 View 的时候,是要重写 onDraw 方法进行绘制的)。ViewGroup 默认是开启这个优化标记的。当然如果明确 ViewGroup 是要通过 onDraw 方法进行绘制的时候,要手动关闭这个标记( setWillNotDraw(false)
)。
示例:
我们自定义一个控件,继承 ViewGroup,重写 onDraw 方法。
代码语言:javascript复制public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
setWillNotDraw(false);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
//这里如果不调用这句话,我们在使用的时候,onDraw 方法不会被调用
setWillNotDraw(false);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//onLayout 在这里必须重写,因为在 ViewGroup 中 onLayout是一个抽象方法
}
//重写 onDraw 方法
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
}
}
xml 中使用
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.ownchan.miclock.study.MyViewGroup
android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="match_parent"/>
</FrameLayout>
当我们的自定义控件在继承 ViewGroup 的时候,如果需要重写 onDraw 方法进行绘制,需要执行 setWillNotDraw(false)
。
推荐一个详解 draw 和 onDraw 调用时机好文: 你真的了解Android ViewGroup的draw和onDraw的调用时机吗