Skip to content

Latest commit

 

History

History
233 lines (150 loc) · 12.6 KB

绘制顺序.md

File metadata and controls

233 lines (150 loc) · 12.6 KB

前面几期讲的是用哪些 API 可以绘制什么内容。接下来要讲的是怎么去安排这些绘制。

第一期:绘制顺序

Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。比如你在重 叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果肯定是不同的:

1 super.onDraw() 前 or 后?

之前写的自定义绘制,全都是直接继承 View 类,然后重写它的 onDraw() 方 法,把绘制代码写在里面,就像这样:

public class AppView extends View {
 ...
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 ... // 自定义绘制代码
 }
 ...
}

这是自定义绘制最基本的形态:继承 View 类,在 onDraw() 中完全自定义它的绘 制。

在之前的样例中,所有的绘制代码全都写在了 super.onDraw() 的下面。不过其实, 绘制代码写在 super.onDraw() 的上面还是下面都无所谓,甚至,把 super.onDraw() 这行代码删掉都没关系,效果都是一样的——因为在 View 这个 类里, onDraw() 本来就是空实现:

所以,除了继承 View 类,自定义绘制更为常见的情况是,继承一个具有某种功能 的控件(比如EditText),去重写它的 onDraw() ,在里面添加一些绘制代码,做出一个「进化版」 的控件:

而这种基于已有控件的自定义绘制,就不能不考虑 super.onDraw() 了:你需要根 据自己的需求,判断出你绘制的内容需要盖住控件原有的内容还是需要被控件原有 的内容盖住,从而确定你的绘制代码是应该写在 super.onDraw() 的上面还是下 面。

1.1 写在 super.onDraw() 的下面

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

这是最为常见的情况:为控件增加点缀性内容。比如,在 Debug 模式下绘制出 ImageView 的图像尺寸信息:

public class AppImageView extends ImageView {
 ...
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 if (DEBUG) {
 // 在 debug 模式下绘制出 drawable 的尺寸信息
 ...
 }
 }
}

1.2 写在 super.onDraw() 的上面

如果把绘制代码写在 super.onDraw() 的上面,由于绘制代码会执行在原有内容的 绘制之前,所以绘制的内容会被控件的原内容盖住。

相对来说,这种用法的场景就会少一些。不过只是少一些而不是没有,比如你可以 通过在文字的下层绘制纯色矩形来作为「强调色」

public class AppTextView extends TextView {
 ...
 protected void onDraw(Canvas canvas) {
 ... // 在 super.onDraw() 绘制文字之前,先绘制出被强调的文字的背景
 super.onDraw(canvas);
 }
}

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

到目前为止只提到了 onDraw() 这一个绘制方法。但其实绘制方法 不是只有一个的,而是有好几个,其中 onDraw() 只是负责自身主体内容绘制的。 而有的时候,你想要的遮盖关系无法通过 onDraw() 来实现,而是需要通过别的绘 制方法。 例如,你继承了一个 LinearLayout ,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为 点缀:

public class SpottedLinearLayout extends LinearLayout {
 ...
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 ... // 绘制斑点
 }
}

但是你会发现,当你添加了子 View 之后,你的斑点不见了:

造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会 先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面 这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。

具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个 绘制方法叫做:dispatchDraw() 。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方 法来绘制子 View。

回到刚才的问题:怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让 它的绘制代码在子 View 的绘制之后再执行就好了。

2.1 写在 super.dispatchDraw() 的下面

只要重写 dispatchDraw() ,并在 super.dispatchDraw() 的下面写上你的绘制代 码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。

public class SpottedLinearLayout extends LinearLayout {
 ...
 // 把 onDraw() 换成了 dispatchDraw()
 protected void dispatchDraw(Canvas canvas) {
super dispatchDraw(canvas);
 super.dispatchDraw(canvas);
 ... // 绘制斑点
 }

2.2 写在 super.dispatchDraw() 的上面

同理,把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主 体内容和子 View 之间。而这个其实和前面 1.1 讲的,重写 onDraw() 并把绘制代码写在 super.onDraw() 之后的 做法,效果是一样的。

3 绘制过程简述

绘制过程中最典型的两个部分是上面讲到的主体和子 View,但它们并不是绘制过程 的全部。除此之外,绘制过程还包含一些其他内容的绘制。具体来讲,一个完整的 绘制过程会依次绘制以下几个内容:

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

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出 这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和 子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会 再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。

这其中的第 2、3 两步,前面已经讲过了;第 1 步——背景,它的绘制发生在一个 叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,如果 要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属 性以及 Java 代码的 View.setBackgroundXxx() 方法),而不能自定义绘制;而第 4、5 两步——滑动边缘渐变和滑动条以及前景,这 两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。

滑动边缘渐变和滑动条可以通过 xml 的 android:scrollbarXXX 系列属性或 Java 代码的 View.setXXXScrollbarXXX() 系列方法来设置;前景可以通过 xml 的 android:foreground 属性或 Java 代码的 View.setForeground() 方法来设置。 而重写 onDrawForeground() 方法,并在它的 super.onDrawForeground() 方法 的上面或下面插入绘制代码,则可以控制绘制内容和滑动边缘渐变、滑动条以及前 景的遮盖关系。

4 onDrawForeground()

这个方法是 API 23 才引入的,所以在重写这个方法的时候要 确认你的 minSdk 达到了 23,不然低版本的手机装上你的软件会没有效果。

在 onDrawForeground() 中,会依次绘制滑动边缘渐变、滑动条和前景。所以如果 你重写 onDrawForeground()

4.1 写在 super.onDrawForeground() 的下面

如果你把绘制代码写在了 super.onDrawForeground() 的下面,绘制代码会在滑 动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变滑条和前景。

public class AppImageView extends ImageView {
 ...
 public void onDrawForeground(Canvas canvas) {
 super.onDrawForeground(canvas);
 ... // 绘制「New」标签
 }
}

4.2 写在 super.onDrawForeground() 的上面

如果你把绘制代码写在了 super.onDrawForeground() 的上面,绘制内容就会在 dispatchDraw() 和 super.onDrawForeground() 之间执行,那么绘制内容会盖 住子 View,但被滑动边缘渐变、滑动条以及前景盖住:

public class AppImageView extends ImageView {
 ...
 public void onDrawForeground(Canvas canvas) {
 ... // 绘制「New」标签
 super.onDrawForeground(canvas);
 }
}

4.3 想在滑动边缘渐变、滑动条和前景之间插入绘制 代码?

不行。 虽然这三部分是依次绘制的,但它们被一起写进了 onDrawForeground() 方法里, 所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们 之间插入绘制,是做不到的。

5 draw() 总调度方法

除了 onDraw() dispatchDraw() 和 onDrawForeground() 之外,还有一个可以 用来实现自定义绘制的方法: draw() 。

draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方 法里。前面的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都 是在 draw() 方法里的。

// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):
public void draw(Canvas canvas) {
 ...
 drawBackground(Canvas); // 绘制背景(不能重写)
 onDraw(Canvas); // 绘制主体
 dispatchDraw(Canvas); // 绘制子 View
 onDrawForeground(Canvas); // 绘制滑动相关和前景
 ...
}

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

5.1 写在 super.draw() 的下面

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

它的效果和重写 onDrawForeground() ,并把绘制代码写在 super.onDrawForeground() 下面时的效果是一样的:都会盖住其他的所有内容。

虽说它们效果一样,但如果既重写 draw() 又重写 onDrawForeground() ,那么 draw() 里的内容还是会盖住 onDrawForeground() 里的内容的。所以严格来讲,它们的效果还是有一点点 不一样的。

5.2 写在 super.draw() 的上面

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

可能觉得没用,但是其实还是有一点用的。

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

在这种时候,就可以重写它的 draw() 方法,然后在 super.draw() 的上方插入 代码,以此来在所有内容的底部涂上一片绿色:

public AppEditText extends EditText {
 ...
 public void draw(Canvas canvas) {
 canvas.drawColor(Color.parseColor("#66BB6A")); // 涂上绿色
 super.draw(canvas);
 }
}

注意

关于绘制方法,有两点需要注意

  1. 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw() ,以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout )并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可 能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView )。
  2. 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一 个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写 在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因 为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重 复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。

2020 7.6 15:37