自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)

前言:
不应该一路失望
又一路等待
时间它说
世界还有不同的海
但不要告诉我
现实它很坏
我想看看
自己的能耐
——莫文蔚《境外》


一、GPU硬件加速

1、概述

GPU英文全称Graphic Processing Unit,中文翻译为“图形处理器”。与CPU不同,GPU是专门为处理图形任务而产生的芯片。
在GPU出现之前,CPU一直负责着所有的运算工作,CPU的架构是有利于X86指令集的串行架构,CPU从设计思路上适合尽可能快的完成一个任务。但当面对类似多媒体、图形图像处理类型的任务时,就显得力不从心。因为在多媒体计算中通常要求更高的运算密度、多并发线程和频繁地存储器访问;显然当你打游戏时,屏幕上的动画是需要实时刷新的,这些都需要频繁的计算、存取动作;如果CPU不能及时响应,那么屏幕就会显得很卡……你的队友可能会发一句……我等的花都谢了,你咋还不动呢……
为了专门处理多媒体的计算、存储任务,GPU就应运而生了,GPU中自带处理器和存储器,以用来专门计算和存储多媒体任务。
对于Andorid来讲,在API 11之前是没有GPU的概念的,在API 11之后,在程序集中加入了对GPU加速的支持,在API 14之后,硬件加速是默认开启的!我们可以显式地强制图像计算时使用GPU而不使用CPU.
在CPU绘制和GPU绘制时,在流程上是有区别的:
在基于软件的绘制模型下,CPU主导绘图,视图按照两个步骤绘制:
  • 让View层次结构失效
  • 绘制View层次结构
在基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤绘制:
  • 让View层次结构失效
  • 记录、更新显示列表
  • 绘制显示列表
可以看到在GPU加速时,流程中多了一项“记录、更新显示列表”,它表示在第一步View层次结构失效后,并不是直接开始逐层绘制,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。所以在GPU加速时,实际是使用OpenGL的函数来完成绘制的。
所以使用GPU加速的优点显而易见:硬件加速提高了Android系统显示和刷新的速度;
它有缺点也显而易见:
1、 兼容性问题:由于是将绘制函数转换成OpenGL命令来绘制,定然会存在OpenGL并不能完全支持原始绘制函数的问题,所以这就会造成在打开GPU加速时,效果会失效的问题。
2、内存消耗问题:由于需要OpenGL的指令,所以需要把系统中的OpenGL相关的包加载到内存中来,所以单纯OpenGL API调用就会占用8MB,而实际上会占用更多内存;
3、电量消耗问题:多使用了一个部件,当然会更耗电……

下图显示了一些特殊函数硬件加速开始支持的平台等级:(红叉表示任何平台都不支持,不在列表中的默认在API 11就开始支持)



自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第1张图片

图片摘自《google官方文档:硬件加速》
我再重复一遍,上面我们涉及了两个API等级,在API 11以后,在程序集中加入了对GPU加速的支持,在API 14之后,硬件加速是默认开启的!也就是说在API 11——API 13虽然是支持硬件加速的,但是默认是关闭的。

2、禁用GPU硬件加速方法

那么问题就来了,如果你的APP跑在API 14版本以后,而你洽好要用那些不支持硬件加速的函数要怎么办?
那就只好禁用硬件加速喽,针对不同类型的东东,Android给我们提供了不同的禁用方法:
硬件加速分全局(Application)、Activity、Window、View 四个层级

1.在AndroidManifest.xml文件为application标签添加如下的属性即可为整个应用程序开启/关闭硬件加速:

<application android:hardwareAccelerated="true" ...> 
2.在Activity 标签下使用 hardwareAccelerated 属性开启或关闭硬件加速:
<activity android:hardwareAccelerated="false" />  
3. 在Window 层级使用如下代码开启硬件加速:(Window层级不支持关闭硬件加速)
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
4.View 级别如下关闭硬件加速:(view 层级上不支持开启硬件加速)
setLayerType(View.LAYER_TYPE_SOFTWARE, null);  
或者使用android:layerType=”software”来关闭硬件加速:比如
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:paddingLeft="2dp"
    android:layerType="software"
    android:paddingRight="2dp" >

二、setXfermode(Xfermode xfermode)之AvoidXfermode

这个函数是图像混合里最难的一个了,它的功能也是相当强大的,这个模式叫做图形混合模式。
与setColorFilter一样,派生自Xfermode的有三个类:

1、概述——基本流程

从上面可以看出,派生自Xfermode的有AvoidXfermode,PixelXorXfermode,PorterDuffXfermode;
从硬件加速不支持的函数列表中,我们可以看到AvoidXfermode,PixelXorXfermode是完全不支持的,而PorterDuffXfermode是部分不支持的。


所以在使用Xfermode时,为了保险起见,我们需要做两件事:
1、禁用硬件加速:

setLayerType(View.LAYER_TYPE_SOFTWARE, null);  
2、使用离屏绘制

//新建图层
int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);

//TODO 核心绘制代码

//还原图层
canvas.restoreToCount(layerID);
有关离屏绘制的原因,这节就先不给大家引申了,后面会单独拉出来两篇文章讲离屏绘制,大家只需要知道,我们需要把绘制的核心代码放在saveLayer()和restoreToCount()之间即可。
下面我们先简单讲解AvoidXfermode的用法,然后写个例子,看下SetXfermode()的使用方法和效果
AvoidXfermode的声明如下:
public AvoidXfermode(int opColor, int tolerance, Mode mode)
当Mode取Mode.TARGET时,它的意义表示将opColor参数所指定的颜色替换成画笔的颜色。
  • 第一个参数opColor:一个16进制的AARRGGBB的颜色值; 
  • 第二个参数tolerance:表示容差,这个概念我们后面再细讲 
  • 第三个参数mode:取值有两个Mode.TARGET和Mode.AVOID;这里我们先知道Mode.TARGET的意义就可以了,Mode.TARGET表示将指定的颜色替换掉 
下面我们还在这个小狗身上做文章,源图片如下:
自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第2张图片

然后我们把小狗身上白色的地方换成红色
效果图如下:

自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第3张图片

看下代码实现:

public class MyView extends View {
    private Paint mPaint;
    private Bitmap mBmp;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

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

        int width  = 500;
        int height = width * mBmp.getHeight()/mBmp.getWidth();
        mPaint.setColor(Color.RED);

        int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);

        canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
        mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
        canvas.drawRect(0,0,width,height,mPaint);

        canvas.restoreToCount(layerID);
    }
}
除了禁用硬件加速和离屏绘制,最关键的代码就是在离屏绘制中间的部分:
canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
canvas.drawRect(0,0,width,height,mPaint);
这段代码只有三句话,看起来很容易理解的样子,其实不然……下面我们就来看看它是怎么来做的吧。
第一句:画一个小狗的bitmap
canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
所以此时的屏幕应该是这样子的:
自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第4张图片
第二句:是找选区

mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
这一点与Photoshop是类似的,就是以白色为目标色,容差为100找到对应的选区;
容差是以颜色差异为基础的,任何两个颜色之间的颜色差异是从0-255的范围内的。具体两个颜色之间的差异的的数值为多少是需要靠公式来计算的: 《维基百科:颜色差异》
而容差的概念就是指与目标色所能容忍的最大颜色差异,所以容差越大,所覆盖的颜色区域就越大;所以当容差为0时,就表示只选择与目标色一模一样的颜色区域;当容差为100时,就表示与目标色值的颜色差异在100范围内的都是可以的;而由于最大的颜色差异是255,所以当我们的容差是255时,所有的颜色都将被选中;
我们使用Photoshop来演示下:
在Photoshop中,有个魔棒工具,它有一个容差的参数,默认是0;指的是只与目标色一致的颜色。我们分别看下当容差为100和容差为255的区域选择范围:

从效果图中可以看出,当容差为100时,只选中白色周边的颜色,而当容差为255时,会选中全图;
第三句:将目标图像对应选区的图像更新到原图上
在拿到选区以后,我们又画了一个纯红的与小狗图片一样大的图片:
canvas.drawRect(0,0,width,height,mPaint);
这句的意思就是把这个纯红色对应选区的图片截取后,覆盖到原图片上面
下面我们使用Photoshop来演示下这个过程:

最后我们再来对比下我们代码产生的效果图:

自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第5张图片

看起来差不多,但是使用photoshop选中的区域会更多一点。这是为什么呢?
这是因为android中计算颜色差值的算法与photoshop的不一样。
前面我们讲了颜色差异的最大值是255,所以当容差为255时,选区应该是整个小狗图片;所以做出来的效果应该是红色会把整个图片覆盖,效果图应该是如下的:

自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第6张图片

使用photoshop来演示这个过程如下:


而使用代码出来的情况却是这样的:

自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第7张图片

对应的代码如下

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

    int width  = 500;
    int height = width * mBmp.getHeight()/mBmp.getWidth();
    mPaint.setColor(Color.RED);


    int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);

    canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
    mPaint.setXfermode(new AvoidXfermode(Color.WHITE,255, AvoidXfermode.Mode.TARGET));
    canvas.drawRect(0,0,width,height,mPaint);

    canvas.restoreToCount(layerID);
}
明显可以看出,红色并没有完全覆盖整个图片,仍然也只是一部分;这就是Android比较蛋疼的地方,做出来的效果与Photoshop不一致,这就因为android中计算颜色差值的算法与photoshop的不一样,photoshop中当容差为255时表示选中所有颜色,而在android中却不是!所以选区的大小也只能靠猜……至于Android是如何来计算两个颜色之间差异的,我也懒得去找了,在现实使用中的地方比较少,一般会用来用一个图片替换另一个图片中的一部分,比如上面的,我们需要把两张图片溶合

我另外做了一张图片,加了些半透明的小红花,图片背景色设为白色;

自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第8张图片

然后利用代码将第二张图片替换小狗身上的白色位置:

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

    int width  = 500;
    int height = width * mBmp.getHeight()/mBmp.getWidth();
    mPaint.setColor(Color.RED);

    int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);

    canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
    mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(),R.drawable.flower_2),null,new Rect(0,0,width,height),mPaint);

    canvas.restoreToCount(layerID);
}
效果图如下:

就效果图来看,替换效果还是可以的。

2、canvas脏区域更新原理

上面我们讲到“第三句:将目标图像对应选区的图像更新到原图上”时讲到,在拿到选区以后,把第二张图片所对应的选区的图片直接截取后,覆盖到原图片上面
其实,这个“覆盖”是不正确的,其实Android在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。
所以对于AvoidXfermode而言,这个规则就是先把把目标区域(选区)中的颜色值先清空,然后再把目标颜色给替换上;
我们再来做个示例,我们把Activity的背景色设置为纯蓝色,而我们如果把选区设置为透明色以后,结果会怎样
(1)、先将Activity的背景色设置为纯蓝色

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:background="#0000ff"
        >

    <com.harvic.BlogXMode.MyView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
</LinearLayout>
(2)、然后将选区填充为纯透明
public class MyView extends View {
    private Paint mPaint;
    private Bitmap mBmp;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

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

        int width  = 500;
        int height = width * mBmp.getHeight()/mBmp.getWidth();

        int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);

        canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
        mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
        //将画笔设置为纯透明
        mPaint.setARGB(0x00,0xff,0xff,0xff);
        canvas.drawRect(0,0,width,height,mPaint);

        canvas.restoreToCount(layerID);
    }
}
效果图如下:
自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一)_第9张图片

这段代码不难理解,所以正是由于把选区改为了全透明,所以才露出底部Activity的背景色。
所以这也证明了,我们提到的canvas的脏区域更新原理:
如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。
所以对于AvoidXfermode而言,这个规则就是先把把目标区域(选区)中的颜色值先清空,然后再把目标颜色给替换上;














你可能感兴趣的:(自定义控件三部曲之绘图篇(十)——Paint之setXfermode(一))