Android图像处理——Paint之ColorFilter

转载请注明出处:http://blog.csdn.net/allen315410/article/details/45059989

平时在Android开发中,一般不太可能频繁使用到Paint——画笔。但是在某些特殊的情况下,例如 自定义控件(继承View)的时候,有时候就需要请出画笔在画布(Canvas,将下篇文章中讲述Canvas)上像“画”出我们想要的元素。然而,很多开发人员对画笔Paint的了解知之甚少,包括我自己在内,所以在今天在这里总结一下Paint的在Android图像处理中起到的作用和用法。


Paint的简单了解

同样,我们学习Paint之前,首先查看一下Paint类的API,其中,我们最需要关注的地方就是Paint类给我们提供了很多的setter方法,可以通过调用setter方法来设置自己的偏好。下面是在Android Studio中查到的Paint类的部分setter:

技术分享

好,可以看到Paint对外提供的方法还是非常丰富的,下面挑几个经常使用到的介绍一下:

Paint(int flags):构建Paint实例,常用的flags是ANTI_ALIAS_FLAG,消除锯齿。

set(Paint src):将另一个Paint复制给当前Paint实例,不多说了。

setAntiAlias(boolean aa):是否消除锯齿。

setColor(int color):设置画笔的颜色。

setColorFilter(ColorFilter filter):设置色彩过滤器,很重要,后面还会有一些总结。

setStrokeWidth(float width):设置Paint划线的宽度。

setStyle(Paint.Style style):设置Paint的风格。 画笔样式分三种:

                                                     1.Paint.Style.STROKE:描边 。2.Paint.Style.FILL_AND_STROKE:描边并填充 。3.Paint.Style.FILL:填充 。

setXfermode(Xfermode xfermode):设置Paint的模式,后面有详细点的说明,很重要。

下面,我们来自定义一个View来尝试一下使用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);
        /**
         *设置画笔样式为描边
         * 画笔样式分三种:
         * 1.Paint.Style.STROKE:描边
         * 2.Paint.Style.FILL_AND_STROKE:描边并填充
         * 3.Paint.Style.FILL:填充
         */
        mPaint.setStyle(Paint.Style.FILL);
        //设置描边的粗细,单位:像素px 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素
        mPaint.setStrokeWidth(20);
        //设置画笔颜色为自定义颜色
        mPaint.setColor(Color.argb(255,255,128,102));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //画一个圆形,取屏幕中心点为圆心
        canvas.drawCircle(ScreenUtil.getScreenW(mContext)/2,
                ScreenUtil.getScreenH(mContext)/2,100,mPaint);
    }
}
好了,以上就是绘制一个圆形的简单代码,我们在布局文件中引用一下这个自定义控件,运行一下。

技术分享

效果如上,一个很圆很圆的圆形啊,等等,貌似看着有点像岛国的旗子,好吧!待会就换了它。

ColorFilter

上面说过了,我们将会这里介绍一下setColorFilter(ColorFilter filter)的用法。setColorFilter(ColorFilter filter)内接收一个参数ColorFilter,在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的文档:

技术分享

一目了然了,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的值:

技术分享

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

技术分享

通过上面的计算,我们得到了最终的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);
    }
}
技术分享

好了图片加载完毕,代码没有难度,上面的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);
技术分享

好吧图片变成黑白的了,难道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);
技术分享

这样的效果是不是有点像负片效果啊!好了,反正我也不懂图像学,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);
运行之后的结果,蓝天没了。

技术分享


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,有张效果图:

技术分享

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

技术分享

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

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
    取两图层全部区域,交集部分变为透明色


关于ColorFilter,目前我学了这些,现在也就总结这些内容吧,欢迎大家一起探讨学习。另外图片混排模式真的很重要,搞懂它也是很有必要的,所以我将会继续学习。图片混排模式在ColorFilter中用的并不是很多,而在Xfermode会并频繁使用到,下一篇,我将自己的对于Xfermode的学习总结一下。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。