HenCoder Android 自定义 View 1-5: 绘制顺序

这期是 HenCoder 自定义绘制的第 1-5 期:绘制顺序java

以前的内容在这里:
HenCoder Android 开发进阶 自定义 View 1-1 绘制基础
HenCoder Android 开发进阶 自定义 View 1-2 Paint 详解
HenCoder Android 开发进阶 自定义 View 1-3 文字的绘制
HenCoder Android 开发进阶 自定义 View 1-4 Canvas 对绘制的辅助android

若是你没据说过 HenCoder,能够先看看这个:
HenCoder:给高级 Android 工程师的进阶手册git

简介

前面几期讲的是「术」,是「用哪些 API 能够绘制什么内容」。到上一期为止,「术」已经讲完了,接下来要讲的是「道」,是「怎么去安排这些绘制」。github

这期是「道」的第一期:绘制顺序。canvas

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

而在实际的项目中,绘制内容相互遮盖的状况是很广泛的,那么怎么实现本身须要的遮盖关系,就是这期要讲的内容。布局

1 super.onDraw() 前 or 后?

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

public class AppView extends View {
    ...

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ... // 自定义绘制代码
    }

    ...
}复制代码

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

在以前的样例中,我把绘制代码全都写在了 super.onDraw() 的下面。不过其实,绘制代码写在 super.onDraw() 的上面仍是下面都无所谓,甚至,你把 super.onDraw() 这行代码删掉都不要紧,效果都是同样的——由于在 View 这个类里,onDraw() 原本就是空实现:优化

// 在 View.java 的源码中,onDraw() 是空的
// 因此直接继承 View 的类,它们的 super.onDraw() 什么也不会作
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    ...

    /** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */
    protected void onDraw(Canvas canvas) {
    }

    ...
}复制代码

然而,除了继承 View 类,自定义绘制更为常见的状况是,继承一个具备某种功能的控件,去重写它的 onDraw() ,在里面添加一些绘制代码,作出一个「进化版」的控件:

基于 EditText,在它的基础上增长了顶部的 Hint Text 和底部的字符计数。

而这种基于已有控件的自定义绘制,就不能不考虑 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 以后,你的斑点不见了:

<SpottedLinearLayout android:orientation="vertical" ... >

    <ImageView ... />

    <TextView ... />

</SpottedLinearLayout>复制代码

形成这种状况的缘由是 Android 的绘制顺序:在绘制过程当中,每个 ViewGroup 会先调用本身的 onDraw() 来绘制完本身的主体以后再去绘制它的子 View。对于上面这个例子来讲,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成以后,先前绘制的斑点就被子 View 盖住了。

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

注:虽然 ViewViewGroup 都有 dispatchDraw() 方法,不过因为 View 是没有子 View 的,因此通常来讲 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。

回到刚才的问题:怎样才能让 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);

       ... // 绘制斑点
    }
}复制代码

好萌的蝙蝠侠啊

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 有主体,有可能会再加上一层半透明的前景做为遮罩,那么它的前景也会在主体以后进行绘制。须要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;以前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

这其中的第 二、3 两步,前面已经讲过了;第 1 步——背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你若是要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法,这个每一个人都用得很 6 了),而不能自定义绘制;而第 四、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」标签
    }
}复制代码
<!-- 使用半透明的黑色做为前景,这是一种很常见的处理 -->
<AppImageView ... android:foreground="#88000000" />复制代码

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

4.2 写在 super.onDrawForeground() 的上面

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

public class AppImageView extends ImageView {
    ...

    public void onDrawForeground(Canvas canvas) {
       ... // 绘制「New」标签

       super.onDrawForeground(canvas);
    }
}复制代码

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

这种写法,和前面 2.1 讲的,重写 dispatchDraw() 并把绘制代码写在 super.dispatchDraw() 的下面的效果是同样的:绘制内容都会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住。

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 加一个绿色的底,我不能使用给它设置绿色背景色的方式,由于这就至关因而把它的背景替换掉,从而会致使下面的那条横线消失:

<EditText ... android:background="#66BB6A" />复制代码

EditText:我究竟是个 EditText 仍是个 TextView?傻傻分不清楚。

在这种时候,你就能够重写它的 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() 一个方法。

总结

今天的内容就是这些:使用不一样的绘制方法,以及在重写的时候把绘制代码放在 super.绘制方法() 的上面或下面不一样的位置,以此来实现须要的遮盖关系。下面用一张图和一个表格总结一下:

嗯,上面这张图在前面已经贴过了,不用比较了彻底同样的。

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

  1. ViewGroup 的子类中重写除 dispatchDraw() 之外的绘制方法时,可能须要调用 setWillNotDraw(false)
  2. 在重写的方法有多个选择时,优先选择 onDraw()

练习项目

为了不转头就忘,强烈建议你趁热打铁,作一下这个练习项目:HenCoderPracticeDraw5

这里放上这期练习项目的图

下期预告

下期是「道」的第二期:动画。

原本没想讲动画的,由于动画其实不属于自定义 View 的范畴。不过最近从各个渠道的反馈里发现有不少人对动画的掌握都比较模糊,而动画若是掌握得很差,自定义 View 的开发确定也会受到限制。因此好吧,增长一期动画详解。

顺便说一下,「道」一共有三期。在这三期事后,自定义 View 的第一部分:自定义绘制就结束了。

预告图?什么预告图?不存在的。

以为赞?

若是你看完以为有收获,把文章转发到你的微博、微信群、朋友圈、公众号,让其余须要的人也看到吧。

相关文章
相关标签/搜索