Android 手把手进阶自定义View(八)- draw 绘制过程解析

一、前言


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

而在实际的项目中,绘制内容相互遮盖的情况是很普遍的,那么怎么实现自己需要的遮盖关系,我们下面来学习以下。

 

二、draw 过程解析


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

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

public void draw(Canvas canvas) {
    ...
    drawBackground(Canvas); // 绘制背景(不能重写)
    onDraw(Canvas); // 绘制主体
    dispatchDraw(Canvas); // 绘制子 View
    onDrawForeground(Canvas); // 绘制滑动相关和前景
    ...
}

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

第 1 步 - 绘制背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法),而不能自定义绘制。

第 2 步 - 绘制主体,发生在 onDraw() 方法里,这个方法在 View 和 ViewGroup 里都是空实现,因此自定义时需要复写。

第 3 步 - 绘制子 View,发生在 dispatchDraw() 方法里,由于单一 View 无子 View,故在 View 中此方法默认为空实现,而在 ViewGroup中系统已经复写好此方法对其子视图进行绘制因此我们不需要复写。

第 4、5 两步 - 滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。

下面我们来看下 ViewGroup 的 dispatchDraw() 方法:

protected void dispatchDraw(Canvas canvas) {
    ......
    // 1. 遍历子View
    final int childrenCount = mChildrenCount;
    ......
    for (int i = 0; i < childrenCount; i++) {
        ...
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
          // 2. 绘制子View视图
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        ...
    }
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    // 最终还是调用了子 View 的 draw ()进行子View的绘制
    return child.draw(canvas, this, drawingTime);
}

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

 

三、绘制顺序


3.1 onDraw() 

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

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

自定义 View 时,绘制代码写在 super.onDraw() 的上面还是下面都无所谓,甚至把 super.onDraw() 这行代码删掉都没关系,效果都是一样的,因为在 View 这个类里 onDraw() 本来就是空实现。然而,除了继承 View 类,自定义绘制更为常见的情况是,继承一个具有某种功能的控件,去重写它的 onDraw() ,在里面添加一些绘制代码,做出一个「进化版」的控件。比如基于 EditText,在它的基础上增加了顶部的 Hint Text 和底部的字符计数。如下图:

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

3.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 的尺寸信息
            ...
        }
    }
}

效果如下:

3.1.2 写在 super.onDraw() 的上面

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

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

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

 

3.2 dispatchDraw()

例如你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 下插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀。但是你会发现,当你添加了子 View 之后,你的斑点不见了。

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

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

3.2.1 写在 super.dispatchDraw() 的下面

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

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

3.2.2 写在 super.dispatchDraw() 的上面

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

 

3.3 onDrawForeground()

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

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

3.3.1 写在 super.onDrawForeground() 的下面

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

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

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

3.3.2 写在 super.onDrawForeground() 的上面

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

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

由于被半透明黑色遮罩盖住,左上角的标签明显变暗了。这种写法和前面讲的重写 dispatchDraw() 并把绘制代码写在 super.dispatchDraw() 的下面的效果是一样的:绘制内容都会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住。

另外,滑动边缘渐变、滑动条和前景这三部分是依次绘制的,但它们被一起写进了 onDrawForeground() 方法里,所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们之间插入绘制,是做不到的。

 

3.4 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);
    }
}

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

下面总结一下:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值