Android-ColorFilter详细学习

2017/8/13 posted in  Android

前言

在Android Studio中点击进去看一下源码,可以看到ColorFilter里的代码量很少

public class ColorFilter {  
    /** 
     * Holds the pointer to the native SkColorFilter instance. 
     * 
     * @hide 
     */  
    public long native_instance;  
  
    @Override  
    protected void finalize() throws Throwable {  
        try {  
            super.finalize();  
        } finally {  
            destroyFilter(native_instance);  
        }  
    }  
  
    static native void destroyFilter(long native_instance);  
}

由此根据我们的经验,判断ColorFilter可能是个父类,具体实现可能下面还有子类完成的,于是再看Google的文档:

20171012150781638185946.png
20171012150781638185946.png

一目了然了,ColorFilter下有3个子类ColorMatrixColorFilter, LightingColorFilter, PorterDuffColorFilter ,下面逐一学习一下。

ColorMatrixColorFilter

ColorMatrixColorFilter翻译为颜色矩阵过滤器,神马是颜色矩阵?实际上, 安卓中管理色彩矩阵是以RGBA像素点的方式加载到内存的,这些点统一使用ColorMatrix的矩阵来统一管理,矩阵定义为4*5的排列形式。那好,首先来看看ColorMatrixColorFilter的两个构造器:

public ColorMatrixColorFilter(ColorMatrix matrix) {  
        mMatrix.set(matrix);  
        update();  
}  
  
public ColorMatrixColorFilter(float[] array) {  
        if (array.length < 20) {  
            throw new ArrayIndexOutOfBoundsException();  
        }  
        mMatrix.set(array);  
        update();  
}  

ColorMatrixColorFilter中一个构造器需要接收ColorMatrix对象,另一个需要接收一个4*5的float型数组,我们再打开Android Studio追踪一下mMatrix.set()方法,可以看到以上两个构造器里面调的set方法各自实现的方式

public void set(ColorMatrix src) {  
        System.arraycopy(src.mArray, 0, mArray, 0, 20);  
}  
  
public void set(float[] src) {  
        System.arraycopy(src, 0, mArray, 0, 20);  
}  

再追踪一下System.arraycopy()方法:

public static void arraycopy(float[] src, int srcPos, float[] dst, int dstPos, int length)

好了,到这里,其实已经很明白了,ColorMatrixColorFilter构造器中接收的两个不同的参数,实际上底层实现方式都是一样的,都是同样调用System.arraycopy()中带float数组参数的方法。所以我们不必再考虑怎么样去写一个ColorMatrix对象传递给ColorMatrixColorFilter了,实际上我们使用第二个构造器,传递一个float数组,会显得程序更加直观易懂,那么我们就尝试写一个ColorMatrixColorFilter,并且设置给Paint吧.

public class CustomView1 extends View {  
    private Paint mPaint;  
    private Context mContext;  
    public CustomView1(Context context) {  
        this(context, null);  
    }  
  
    public CustomView1(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mContext = context;  
        initPaint();  
    }  
    private void initPaint() {  
        //初始化Paint,并且设置消除锯齿。  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        //设置画笔样式为描边  
        mPaint.setStyle(Paint.Style.FILL);  
        //设置描边的粗细,单位:像素px 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素  
        mPaint.setStrokeWidth(20);  
        //设置画笔颜色为自定义颜色  
        mPaint.setColor(Color.argb(255, 255, 128, 102));  
        ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(new float[]{  
                1, 0, 0, 0, 0,  
                0, 1, 0, 0, 0,  
                0, 0, 1, 0, 0,  
                0, 0, 0, 1, 0  
        });  
        mPaint.setColorFilter(colorFilter);  
    }  
    @Override  
    protected void onDraw(Canvas canvas) {  
        //画一个圆形,取屏幕中心点为圆心  
        canvas.drawCircle(ScreenUtil.getScreenW(mContext) / 2,  
                ScreenUtil.getScreenH(mContext) / 2, 100, mPaint);  
    }  
}

看上面的例子程序,首先创建了一个ColorMatrixColorFilter对象,并且传递进去一个float型的4*5排列的数组,然后调用Paint的setColorFilter方法将ColorMatrixColorFilter对象传入,我们在模拟器上运行一下,duang~~,我去,什么变化都没有,还是岛国的旗帜。这是怎么回事?下面有必要在学习一下了:

其实一个4*5的float数组中分别对应的是RGBA的向量值,第一行代表的是R(红色)的向量值,第二行代表G(绿色)的向量值,第三行代表B(蓝色)的向量值,第四行代表A(透明度)的向量值,这4行分别代表不同的RGBA的向量值,并且值的取值范围是[0.0F , 2.0F],当值为1.0F的时候,表示保持原有的色彩,不发生色彩便宜。so,如果我们想要将上面的红色的圈圈颜色变掉,就不能像上面的代码一样,将所有的向量值都设置为1.0F,下面我们修改一个:

ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(new float[]{  
                0.5F, 0, 0, 0, 0,  
                0, 0.5F, 0, 0, 0,  
                0, 0, 0.5F, 0, 0,  
                0, 0, 0, 1, 0  
});  
mPaint.setColorFilter(colorFilter);  

将上面的ColorMatrixColorFilter中的float数组替换成这样的
哎哟呵~颜色变深了,看起来神奇很多。那么,这个色彩矩阵以及这个float数组是怎样做到的呢?或者说是怎样通过计算后得到另外一个色彩值的呢?下面作图来说明一下,我们顶一个ColorMatrix的4*5的float型数组,然后定义一个我们自己MyColor,分别代表RGBA的值:

20171012150781645122295.jpg
20171012150781645122295.jpg

实际上,安卓系统计算色彩值是用矩阵相乘的方式得出的,如上图的样子。这里的MyColor的各项值都要转换到[0,1]之间的值,下面就是我们实际转换的计算方式和结果了。

20171012150781647527079.jpg
20171012150781647527079.jpg

通过上面的计算,我们得到了最终的RGBA的值是(0.5,0.25,0.2,1),说明RGB色彩值都发生了便宜,只有A未偏移,然后我们将这些值乘以255后还原一下看看,是不是跟上图的圈圈的色彩值是一致的呢,好吧,别看了,肯定必须一定是一样的。那么了解色彩矩阵有什么用呢?上面简单的更换一下色彩值而已,Paint类下也提供了setColor()方法,直接将色彩值设置上去,都TMD的方便,还搞什么玩意的矩阵,显得自己牛逼+蛋疼是不是?解释一下,上面的例子不过是个例子而已啊,真正开发的时候肯定是setColor比较简便嘛。问题来了,我们有可能处理的不是一个纯色彩的东西,而是一直图片呢?一张图片是有几十万中色彩值的,这时候setColor()就不可能让他们变色了吧,还是得用色彩矩阵来搞这玩意。下面我们从drawable目录下加载一张图片吧!

public class CustomView2 extends View {  
  
    private Context mContext;  
    private Paint mPaint;  
    private Bitmap mBitmap;  
    private int x, y;  
  
    public CustomView2(Context context) {  
        this(context, null);  
    }  
  
    public CustomView2(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mContext = context;  
        initRes();  
        initPaint();  
    }  
  
    private void initRes() {  
        //获取图片  
        mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.image);  
        //获取图片显示起始位置  
        x = ScreenUtil.getScreenW(mContext) / 2 - mBitmap.getWidth() / 2;  
        y = ScreenUtil.getScreenH(mContext) / 2 - mBitmap.getHeight() / 2;  
    }  
  
    private void initPaint() {  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawBitmap(mBitmap, x, y, mPaint);  
    }  
}  

20171012150781650576419.png
20171012150781650576419.png

好了图片加载完毕,代码没有难度,上面的Paint没有做任何的处理,下面我们为Paint设置色彩过滤器吧!

ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(new float[]{  
                0.33F, 0.59F, 0.11F, 0, 0,  
                0.33F, 0.59F, 0.11F, 0, 0,  
                0.33F, 0.59F, 0.11F, 0, 0,  
                0, 0, 0, 1, 0,  
});  
mPaint.setColorFilter(colorFilter);  

20171012150781653146217.png
20171012150781653146217.png

好吧图片变成黑白的了,难道setColor()也可以办吗?再改一个试试:

ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(new float[]{  
                1.5F, 1.5F, 1.5F, 0, -1,  
                1.5F, 1.5F, 1.5F, 0, -1,  
                1.5F, 1.5F, 1.5F, 0, -1,  
                0, 0, 0, 1, 0,  
        });  
mPaint.setColorFilter(colorFilter);  

20171012150781655966867.png
20171012150781655966867.png

这样的效果是不是有点像负片效果啊!好了,反正我也不懂图像学,ColorMatrixColorFilter想设置什么样的就改改矩阵就行了,到底需要什么效果,效果的值要设置成多少,我也不知道,问问美工吧!我们只负责写程序!呵呵~~

LightingColorFilter

LightingColorFilter顾名思义就是“光照色彩过滤器”,就是模拟一个光照照过图像所产生的效果,构造器是这样的:
public LightingColorFilter(int mul, int add)

查看一下Google文档,是这样介绍滴:

光照色彩滤光片,可以用来模拟简单的照明效果。一个lightingcolorfilter定义了两个参数,一个用于与源颜色相乘(称为colormultiply)和一个用于添加到源颜色(称为coloradd)。alpha通道是原封不动的彩色滤光片。给定一个源颜色的RGB,由此产生的特定颜色计算如下:

R' = R * colorMultiply.R + colorAdd.R
G' = G * colorMultiply.G + colorAdd.G
B' = B * colorMultiply.B + colorAdd.B

每个通道值的结果范围是0~255。上面的介绍写的比较明白, 算法也很简单,我们以上面的原图为例,看见蓝天了吗,我们现在去掉这个蓝色的天。根据这个短发描述呢,我们仅仅去掉蓝色,就要将蓝色的通道值改变,将B计算为其它值,这时候colorMultiply.B = 00,colorAdd.B =00,计算得到的B = 00,其它的通道R和G均不变,那么,colorAdd.R=0,colorAdd.G =0;colorMultiply.R = FF,colorMultiply.G =FF,Alpha通道A是忽略的,所以随便设置什么都不会有变化的。

LightingColorFilter colorFilter = new LightingColorFilter(0xFFFFFF00, 0x00000000);  
mPaint.setColorFilter(colorFilter);

运行之后的结果,蓝天没了。

20171012150781662212525.png
20171012150781662212525.png

PorterDuffColorFilter

ColorFilter下还有最后一个子类,PorterDuff混合模式的色彩过滤器,下面是其构造器:
public PorterDuffColorFilter(int color, PorterDuff.Mode mode)

Google文档:PorterDuff滤光器可以用于点源像素使用一个单一的颜色和一个特定的波特达夫复合模式。

PorterDuffColorFilter的构造器也很简单,其中第一个参数表示一个16进制的色彩值,第二个参数是一个枚举值PorterDuff.Mode,表示图片混排的模式,PorterDuff.Mode在Android下一共有16种。下面我们先写一个小例子看一下,这里我们还是使用上面的图片,为原图添加图片混排模式,颜色值设置为红色0XFFFF0000,混排模式设置为PorterDuff.Mode.DARKEN。

public class CustomView2 extends View {  
  
    private Context mContext;  
    private Paint mPaint;  
    private Bitmap mBitmap;  
    private int x, y;  
  
    public CustomView2(Context context) {  
        this(context, null);  
    }  
  
    public CustomView2(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mContext = context;  
        initRes();  
        initPaint();  
    }  
  
    private void initRes() {  
        //获取图片  
        mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.image);  
        //获取图片显示起始位置  
        x = ScreenUtil.getScreenW(mContext) / 2 - mBitmap.getWidth() / 2;  
        y = ScreenUtil.getScreenH(mContext) / 2 - mBitmap.getHeight() / 2;  
    }  
  
    private void initPaint() {  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(0XFFFF0000, PorterDuff.Mode.DARKEN);  
        mPaint.setColorFilter(colorFilter);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawBitmap(mBitmap, x, y, mPaint);  
    }  
}  

上面的图片就是运行之后的效果了,原图不仅变红了,而且还变暗了。其实我们这里将PorterDuffColorFilter的构造器参数拆开来分析一下,首先我们传递进去一个红色的颜色值0XFFFF0000,这里相当于创建了一张新的图层,图层的颜色就是0XFFFF0000,而我们的原图可以看作是第二张图层,我们先把这2个图片重叠放在一起,就会发现得到一个原图上很红的图片,然后我们看一下PorterDuff.Mode是DARKEN模式,表示在之前得到的“原图+很红”的图片上进一步将色调调成暗色,最终得到了如上所示的图片。

关于PorterDuff.Mode,Android系统一共提供了18种混排模式,在模拟器的ApiDemos/Graphics/XferModes,有张效果图:

20171012150781665919671.png
20171012150781665919671.png

这张图可以很形象的说明图片各种混排模式下的效果。其中Src代表原图,Dst代表目标图,两张图片使用不同的混排方式后,得到的图像是如上图所示的。 PorterDuff.Mode也提供了18种混排模式算法,其中比上图多了ADD和OVERLAY两种模式:

20171012150781667837297.png
20171012150781667837297.png

其中Sa全称为Source alpha表示源图的Alpha通道;Sc全称为Source color表示源图的颜色;Da全称为Destination alpha表示目标图的Alpha通道;Dc全称为Destination color表示目标图的颜色,[...,..]前半部分计算的是结果图像的Alpha通道值,“,”后半部分计算的是结果图像的颜色值。图像混排后是依靠这两个值来重新计算ARGB值的,具体计算算法,抱歉,我也不知道,不过不要紧,不了解计算算法也不影响我们程序员写程序的。我们只要对照上面的apiDemo中提供的图片就能推测出混排后的结果的,下面是在网上找到的汉字语言描述,感谢这位作者的总结。

注意:先绘制dst,再绘制src。

  • 1.PorterDuff.Mode.CLEAR    所绘制源图像不会提交到画布上。
  • 2.PorterDuff.Mode.SRC    只显示源图像。
  • 3.PorterDuff.Mode.DST    只显示目标图像。
  • 4.PorterDuff.Mode.SRC_OVER    正常绘制显示,源图像居上显示。
  • 5.PorterDuff.Mode.DST_OVER    上下层都显示。目标图像居上显示。
  • 6.PorterDuff.Mode.SRC_IN    取两层绘制交集中的源图像。
  • 7.PorterDuff.Mode.DST_IN     取两层绘制交集中的目标图像。
  • 8.PorterDuff.Mode.SRC_OUT    只在源图像和目标图像不相交的地方绘制源图像。
  • 9.PorterDuff.Mode.DST_OUT    只在源图像和目标图像不相交的地方绘制目标图像。
  • 10.PorterDuff.Mode.SRC_ATOP    在源图像和目标图像相交的地方绘制源图像,在不相交的地方绘制目标图像。
  • 11.PorterDuff.Mode.DST_ATOP   在源图像和目标图像相交的地方绘制目标图像而在不相交的地方绘制源图像。
  • 12.PorterDuff.Mode.XOR    异或:去除两图层交集部分
  • 13.PorterDuff.Mode.DARKEN     取两图层全部区域,交集部分颜色加深
  • 14.PorterDuff.Mode.LIGHTEN   取两图层全部,点亮交集部分颜色
  • 15.PorterDuff.Mode.MULTIPLY    取两图层交集部分叠加后颜色
  • 16.PorterDuff.Mode.SCREEN    滤色。