笔记—自定义View之绘制顺序

一 绘制方法简单介绍

在自定义View中的绘制方法有以下几种:

1 最常用的onDraw方法,此方法绘制的是View的主体内容

2 绘制子View的dispatchDraw方法,一般在以ViewGroup为父类的View中被用到,此方法绘制的是View中的子view

3 被忽视的onDrawForegound方法,用来滑动边缘渐变,滑动条,前景。

4 方法调度的draw方法 此方法是绘制过程中,上述方法的总方法。

二 绘制方法的分别介绍,以及用法不同带来的效果

1 onDraw() 只是负责自身主体内容绘制 绘制代码super.onDraw() 前 or 后?

1 写在 super.onDraw() 的下面

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

2 写在 super.onDraw() 的上面

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

2 dispatchDraw 负责绘制View中的子view 也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View。注意:虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。绘制代码写在 super.dispatchDraw () 前 or 后?

1 写在 super.dispatchDraw() 的下面

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

2 写在 super.dispatchDraw() 的上面

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

3 onDrawForeground 会依次绘制滑动边缘渐变、滑动条和前景

1 写在 super.onDrawForeground() 的下面

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

2 写在 super.onDrawForeground() 的上面

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

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

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

4 draw() 总调度方法 draw() 是绘制过程的总调度方法

一个 View 的整个绘制过程都发生在 draw() 方法里。前面讲到的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。onDraw() dispatchDraw() onDrawForeground() 这三个方法在 draw() 中被依次调用

1 写在 super.draw() 的下面

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

2 写在 super.draw() 的上面

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

三 绘制方法及过程总结

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

1 背景 2 主体(onDraw()) 3 子 View(dispatchDraw()) 4 滑动边缘渐变和滑动条 5 前景
一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。这其中的第 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() 方法的上面或下面插入绘制代码,则可以控制绘制内容和滑动边缘渐变、滑动条以及前景的遮盖关系

一下两幅图均摘自HenCoder,这里感谢作者的辛苦付出

笔记—自定义View之绘制顺序笔记—自定义View之绘制顺序

四 绘制过程中需要注意的地方

1 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw()

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

2 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。

但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法,换句话说 在效果相同时 能写在onDraw方法中尽量写在onDraw方法中。