Canvas 绘制顺序

参考:https://hencoder.com/ui-1-5/
练习github地址:https://github.com/fandazeng/ViewDemo

在实际的项目中,我们绘制的内容需要考虑绘制的顺序,绘制内容相互遮盖的情况是很普遍的。

1. super.onDraw() 前 or 后?

如果我们自定义View ,是通过直接继承 View 类,然后重写它的 onDraw() 方法,那么绘制代码写在 super.onDraw() 的上面还是下面都无所谓。甚至,你把 super.onDraw() 这行代码删掉都没关系,效果都是一样的——因为在 View 这个类里,onDraw() 本来就是空实现,就是要给子类实现的。但是如果继承自具有某种功能的控件,去重写它的 onDraw() ,在里面添加一些绘制代码,做出一个「进化版」的控件,就要考虑你的绘制代码是应该写在 super.onDraw() 的上面还是下面了。

1.1 写在 super.onDraw() 的下面

把绘制代码写在 super.onDraw() 的下面,由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。

 @Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //代码写在后面
    if (BuildConfig.DEBUG) {
        // 在DEBUG模式下绘制出 drawable 的尺寸信息
        Drawable drawable = getDrawable();
        if (drawable != null) {
            canvas.save();
            canvas.concat(getImageMatrix());
            Rect bounds = drawable.getBounds();
            canvas.drawText(getResources().getString(R.string.image_size, bounds.width(), bounds.height()), 20, 40, mPaint);
            canvas.restore();
        }
    }
}

1.2 写在 super.onDraw() 的上面

由于绘制代码会执行在原有内容的绘制之前,所以绘制的内容会被控件的原内容盖住。

@Override
protected void onDraw(Canvas canvas) {
    //代码写在前面

    //行数索引从0开始
    Layout layout = getLayout();
    mRectF.left = layout.getLineLeft(1) + getPaddingLeft();
    mRectF.top = layout.getLineTop(1);
    mRectF.right = layout.getLineRight(1) + getPaddingLeft();
    mRectF.bottom = layout.getLineBottom(1);

    canvas.drawRect(mRectF, mPaint);

    mRectF.left = layout.getLineLeft(layout.getLineCount() - 4) + getPaddingLeft();
    mRectF.top = layout.getLineTop(layout.getLineCount() - 4);
    mRectF.right = layout.getLineRight(layout.getLineCount() - 4) + getPaddingLeft();
    mRectF.bottom = layout.getLineBottom(layout.getLineCount() - 4);

    canvas.drawRect(mRectF, mPaint);

    super.onDraw(canvas);
}

2 dispatchDraw():绘制子 View 的方法

在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

    // 在superr后面绘制斑点
    mPattern.draw(canvas);
}

如果在 onDraw() 方法中绘制内容,绘制子View时,会覆盖之前的绘制内容。写在 super.dispatchDraw() 的上面和写在 super.onDraw() 之后的做法是一样的效果。

3 绘制过程简述

一个完整的绘制过程会依次绘制以下几个内容:

  1. 背景
  2. 主体(onDraw())
  3. 子 View(dispatchDraw())
  4. 滑动边缘渐变和滑动条
  5. 前景

第 1 步——背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法),而不能自定义绘制;而第 4、5 两步——滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。
需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的。

滑动边缘渐变和滑动条可以通过 xml 的 android:scrollbarXXX 系列属性或 Java 代码的 View.setXXXScrollbarXXX() 系列方法来设置;前景可以通过 xml 的 android:foreground 属性或 Java 代码的 View.setForeground() 方法来设置。

4.1 写在 super.onDrawForeground() 的下面

绘制代码会在滑动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变、滑动条和前景。

@Override
public void onDrawForeground(Canvas canvas) {
    super.onDrawForeground(canvas);

    //这个方法在api23以上才有效

    //代码写在后面
    if (BuildConfig.DEBUG) {
        // 在DEBUG模式下绘制出 drawable 的尺寸信息
        Drawable drawable = getDrawable();
        if (drawable != null) {
            canvas.save();
            canvas.concat(getImageMatrix());
            canvas.drawRect(0,10,100,50,mPaint);
            mPaint.setColor(Color.WHITE);
            canvas.drawText("NEW", 20, 40, mPaint);
            canvas.restore();
        }
    }
}

4.2 写在 super.onDrawForeground() 的上面

@Override
public void onDrawForeground(Canvas canvas) {
    //这个方法在api23以上才有效

    //代码写在后面
    if (BuildConfig.DEBUG) {
        // 在DEBUG模式下绘制出 drawable 的尺寸信息
        Drawable drawable = getDrawable();
        if (drawable != null) {
            canvas.save();
            canvas.concat(getImageMatrix());
            canvas.drawRect(0,10,100,50,mPaint);
            mPaint.setColor(Color.WHITE);
            canvas.drawText("NEW", 20, 40, mPaint);
            canvas.restore();
        }
    }

    super.onDrawForeground(canvas);
}

5 draw() 总调度方法

onDraw() dispatchDraw() onDrawForeground() 这三个方法在 draw() 中被依次调用,因此它们的遮盖关系也就像前面所说的——dispatchDraw() 绘制的内容盖住 onDraw() 绘制的内容;onDrawForeground() 绘制的内容盖住 dispatchDraw() 绘制的内容。而在它们的外部,则是由 draw() 这个方法作为总的调度。所以,你也可以重写 draw() 方法来做自定义的绘制。

5.1 写在 super.draw() 的下面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。效果跟写在 super.onDrawForeground() 下面时是一样的。

5.2 写在 super.draw() 的上面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的上面,那么这段代码会在其他所有绘制之前被执行,所以这部分绘制内容会被其他所有的内容盖住,包括背景。是的,背景也会盖住它。

@Override
public void draw(Canvas canvas) {      //总调度方法

    //代码写在前面
    canvas.drawColor(Color.parseColor("#66BB6A"));

    super.draw(canvas);

}

注意:它下面的那条横线,是 EditText 的背景。所以如果我想给这个 EditText 加一个绿色的底,我不能使用给它设置绿色背景色的方式,因为这就相当于是把它的背景替换掉,从而会导致下面的那条横线消失。

注意

  1. 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。

  2. 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。