[关闭]
@TryLoveCatch 2022-05-07T03:27:27.000000Z 字数 4194 阅读 2198

Android知识体系之自定义View绘制顺序

Android知识体系


Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住.而在实际的项目中,绘制内容相互遮盖的情况是很普遍的,那么怎么实现自己需要的遮盖关系,就是这期要讲的内容。

前言

具体来讲,一个完整的绘制过程会依次绘制以下几个内容:

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

注意:前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

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

背景

主体

子View

滑动边缘渐变和滑动条 和 前景

onDraw()

自定义绘制最基本的形态:继承 View 类,在 onDraw() 中完全自定义它的绘制。
如果我们继承的是View 类,那么写在super.onDraw()上面或者下面是没有区别的,因为View的draw()方法本来就是空方法。
但是,我们更常用到的是,继承一个具有某种功能的控件,去重写它的 onDraw() ,例如继承EditText、继承ImageView等等,这个时候,就需要考虑是super.onDraw()了,根据自己的需求,判断出你绘制的内容需要盖住控件原有的内容还是需要被控件原有的内容盖住。

super.onDraw()下面

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

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

  1. public class AppImageView extends ImageView {
  2. ...
  3. protected void onDraw(Canvas canvas) {
  4. super.onDraw(canvas);
  5. if (DEBUG) {
  6. // 在 debug 模式下绘制出 drawable 的尺寸信息
  7. ...
  8. }
  9. }
  10. }

super.onDraw()上面

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

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

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

dispatchDraw()

介绍dispatchDraw()之前,我们举一个例子,来说明一个问题:

你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀:

  1. public class SpottedLinearLayout extends LinearLayout {
  2. ...
  3. protected void onDraw(Canvas canvas) {
  4. super.onDraw(canvas);
  5. // 绘制斑点
  6. ...
  7. }
  8. }

看起来没问题对吧?

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

  1. <SpottedLinearLayout
  2. android:orientation="vertical"
  3. ... >
  4. <ImageView ... />
  5. <TextView ... />
  6. </SpottedLinearLayout>

为什么呢?

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

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

注意:虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。

super.dispatchDraw()下面

如果把绘制代码写在 super.dispatchDraw() 的下面,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。

  1. public class SpottedLinearLayout extends LinearLayout {
  2. ...
  3. // 把 onDraw() 换成了 dispatchDraw()
  4. protected void dispatchDraw(Canvas canvas) {
  5. super.dispatchDraw(canvas);
  6. ... // 绘制斑点
  7. }
  8. }

这样就达到上图中的“期望的效果”了

super.dispatchDraw()上面

如果把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主体内容和子 View 之间。

而这个,就和上面说的 “super.onDraw()下面”效果是一样的。

onDrawForeground()

这个方法是 API 23 才引入的

super.onDrawForeground()下面

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

  1. public class AppImageView extends ImageView {
  2. ...
  3. public void onDrawForeground(Canvas canvas) {
  4. super.onDrawForeground(canvas);
  5. ... // 绘制「New」标签
  6. }
  7. }
  8. <!-- 使用半透明的黑色作为前景,这是一种很常见的处理 -->
  9. <AppImageView
  10. ...
  11. android:foreground="#88000000" />

左上角的标签并没有被黑色遮罩盖住,而是保持了原有的颜色。

super.onDrawForeground()上面

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

  1. public class AppImageView extends ImageView {
  2. ...
  3. public void onDrawForeground(Canvas canvas) {
  4. ... // 绘制「New」标签
  5. super.onDrawForeground(canvas);
  6. }
  7. }

由于被半透明黑色遮罩盖住,左上角的标签明显变暗了。

这个和在“super.dispatchDraw()下面”的效果是一样的

draw()

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

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

super.draw()下面

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

这个和在“super.onDrawForeground()下面”的效果是一样的,都会盖住其他的所有内容。

super.draw()上面

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

总结

另外别忘了上面提到的那两个注意事项:

参考

自定义 View 1-5 绘制顺序

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注