自定义 View - onDraw 过程详解
之前两篇文章分析了 onMeasure 过程和 onLayout 过程,不熟悉的童鞋可以回头去复习下,本篇文章来分析绘制过程的最后一个 onDraw 过程。这个过程的绘制使用到的 Paint 和 Canvas 在之前也有讲解到,在本篇的练习代码中有使用到,不会具体讲解这些知识点,不熟悉的话可以看看我之前的文章
View 绘制过程
绘制过程也不复杂,在 View draw 方法的源码中注释写的也很详细,先给出一张图。
图中的绘制过程也是代码中给出的绘制过程,对于 单一 View 和 ViewGroup 来说,差别仅仅在于绘制子 View 上, 因为单一 View 没有子 View,所以不需要绘制子 View,即 dispatchDraw(canvas) 是一个空实现;那对于 ViewGroup 来说,如果有子 View 的话,就需要实现该方法,绘制子 View,过程循环遍历子 View,然后调用 子 View 的 draw 方法,这样就回到了子 View 的绘制过程,具体分析看后面。
draw_process.png1.单一 View 绘制
单一 View 的绘制过程就是上面图中的流程,下面主要看下代码中的注释。
public 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)
*/
上面指出绘制过程的 6 个步骤,其中 2 和 5 是对图层的操作,不是必要的
1、绘制背景,该方法发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法
3、绘制主体部分,例如,对于 TextView,绘制文字部分
4、绘制子 View,单一 View 没有子 View,空实现,对于继承 ViewGroup 的 Layout,如果有子 View 需要实现该方法。
6、绘制滑动边缘渐变和滑动条、以及前景,这些东西是在一个方法中绘制的,不能分开
int saveCount;
// Step 1:绘制背景
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:绘制主体部分
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 绘制子 View
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, 绘制装饰部件,滚动条,前景等
onDrawForeground(canvas);
// Step 7, 具有焦点时,绘制焦点高亮
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// 其实到这里已经结束了
return;
}
// 但是源码中后面还有一大堆代码,这些代码一般很少能够执行到,
// 可能是用来测试,后面这部分有兴趣的自己看看吧
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
...
}
上面就是 draw 总调度函数绘制的过程,下面看下每个过程的具体函数。
绘制背景
背景绘制过程采用 Drawable 的 draw 方法,注意这个方法是 private,也就是说背景绘制我们不能重写,如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
// 设定背景的范围
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;
}
}
// background 是一个 Drawable,背景绘制过程采用 Drawable 的 draw 方法
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
onDraw
// 绘制 View 本身的内容
// 继承自 View 的单一 View 实现各不相同,需要自己实现,如 TextView 绘制文字
protected void onDraw(Canvas canvas) {
}
dispatchDraw
/**
* dispatchDraw 用来绘制子 View,
* 因为单一 View 没有子 View,所以不需要绘制子 View,即 dispatchDraw(canvas) 是一个空实现;那对于
* ViewGroup 来说,如果有子 View 的话,就需要实现该方法,绘制子 View,过程循环遍历子 View
*/
protected void dispatchDraw(Canvas canvas) {
... // 空实现
}
onDrawForeground
绘制前景,这个过程也会绘制滚动条和指示器等小装饰,然后绘制前景,前景绘制过程和背景过程类似,也是通过 Drawable 的 draw 方法绘制的,装饰和前景一起绘制,该方法不能拆分。
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);
}
// 绘制前景,前景和背景绘制过程基本一致
foreground.draw(canvas);
}
}
2. View Group 绘制过程
由于 ViewGroup 继承 View,所以上面几个过程基本是一致的,除了 dispatchDraw 方法,一般情况下 ViewGroup 是有子 View 的,所以通过 dispatchDraw 方法来进行子 View 的绘制,下面主要分析一下 dispatchDraw 方法。
@Override
protected void dispatchDraw(Canvas canvas) {
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
...
for (int i = 0; i < childrenCount; i++) {
...
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// 调用子 View 的绘制方法
more |= drawChild(canvas, child, drawingTime);
}
}
}
// 调用子 View 的绘制方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
dispatchDraw 实际上就是对子 View 的遍历,然后依次调用子 View 的绘制方法,就按照上面的 View 的绘制方法执行,当然也有可能还是 ViewGroup,那么还会继续调用 dispatchDraw 方法,遍历子 View。
绘制方法重写顺序分析
上面过程分析了 View 的绘制过程,那么我们进行自定义 View 时,会有这样一个问题,有时我们会只用 super 调用 父类方法,我们自己重写的部分代码,是放在上面,还是下面呢,有何影响?下面就来对上面的方法进行分析,写在 super 上面和下面有何影响。
(1) super.onDraw() 前
public class MyView extends View {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 自定义绘制代码
}
...
}
写在 onDraw 的下面,自定义的绘制部分会盖住控件原有的内容,如在 ImageView 的图片上显示图片的尺寸信息等。
before_draw.jpg(2) super.onDraw() 后
写在 onDraw 的下面,自定义的绘制部分被控件原有的内容盖住,如在文字下面绘制强调色。
after_draw.jpg**(3) ViewGroup 子类的 onDraw() 中****
一个继承自 ViewGroup 的子类,本身在 onDraw 是一个空实现,在 onDraw 中进行绘制,如果没有子 View,那么会显示出绘制的内容,如果有子 View,子 View 会遮住 onDraw 中绘制的内容。
ondraw_layout.jpg摘自 Hencoder
另外有一点,出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)
(4) 在 dispatchDraw 下面
写在 dispatchDraw 下面,会遮住子 View 部分
dispatch_draw.jpg(5) 在onDrawForeground 后
写在 在onDrawForeground 后面会会遮住滑动边缘渐变、滑动条和前景
after_foreground.jpg(6) 在onDrawForeground 前
写在 在onDrawForeground 前面会被前景部分遮住
before_foreground.jpg最后给出一张 Hencoder 大神总结的一张图
draw_position.png