自定义控件其实很简单1/6

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

炮兵镇楼

上一节我们粗略地讲了下如何去实现我们的View并概述了View形成动画的基本原理,这一节我们紧跟上一节的步伐来深挖如何去绘制更复杂的View!

通过上一节的学习我们了解到什么是画布Canvas什么是画笔Paint,并且学习了如何去设置画笔的属性如何在画布上画一个圆,然而,画笔的属性并非仅仅就设置个颜色、大小那么简单而画布呢肯定也不单单只是能画一个圆那么无趣,工欲善其事必先利其器,既然想画好图那必须学会画笔和画布的使用,那么今天我们就来看看Android的画笔跟我们现实中的画笔有什么不同呢?

如上节所说我们可以通过Paint中大量的setter方法来为画笔设置属性:


这些属性大多我们都可以见名知意,很好理解,即便如此,哥还是带大家过一遍逐个剖析其用法,其中会不定穿插各种绘图类比如Canvas、Xfermode、ColorFilter等等的用法。

set(Paint src)

顾名思义为当前画笔设置一个画笔,说白了就是把另一个画笔的属性设置Copy给我们的画笔,不累赘了

setARGB(int a, int r, int g, int b)

不扯了,别跟我说不懂

setAlpha(int a)

同上

setAntiAlias(boolean aa)

这个上一节我们用到了,打开抗锯齿,不过我要说明一点,抗锯齿是依赖于算法的,算法决定抗锯齿的效率,在我们绘制棱角分明的图像时,比如一个矩形、一张位图,我们不需要打开抗锯齿。

setColor(int color)

不扯

setColorFilter(ColorFilter filter)

设置颜色过滤,什么意思呢?就像拿个筛子把颜色“滤”一遍获取我们想要的色彩结果,感觉像是扯蛋白说一样是不是?没事我们慢慢说你一定会懂,这个方法需要我们传入一个ColorFilter参数同样也会返回一个ColorFilter实例,那么ColorFilter类是什么呢?追踪源码进去你会发现其里面很简单几乎没有:

  1. public class ColorFilter {  
  2.     int native_instance;  
  3.   
  4.     /** 
  5.      * @hide 
  6.      */  
  7.     public int nativeColorFilter;  
  8.   
  9.     protected void finalize() throws Throwable {  
  10.         try {  
  11.             super.finalize();  
  12.         } finally {  
  13.             finalizer(native_instance, nativeColorFilter);  
  14.         }  
  15.     }  
  16.   
  17.     private static native void finalizer(int native_instance, int nativeColorFilter);  
  18. }  
压根没有和图像处理相关的方法对吧,那么说明该类必定是个父类或者说其必有一定的子类去实现一些方法,查看API文档发现果然有三个子类:

ColorMatrixColorFilter、LightingColorFilter和PorterDuffColorFilter,也就是说我们在setColorFilter(ColorFilter filter)的时候可以直接传入这三个子类对象作为参数,那么这三个子类又是什么东西呢?首先我们来看看

ColorMatrixColorFilter

中文直译为色彩矩阵颜色过滤器,要明白这玩意你得先了解什么是色彩矩阵。在Android中图片是以RGBA像素点的形式加载到内存中的,修改这些像素信息需要一个叫做ColorMatrix类的支持,其定义了一个4x5的float[]类型的矩阵:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         10000,  
  3.         01000,  
  4.         00100,  
  5.         00010,  
  6. });  
其中,第一行表示的R(红色)的向量,第二行表示的G(绿色)的向量,第三行表示的B(蓝色)的向量,最后一行表示A(透明度)的向量,这一顺序必须要正确不能混淆!这个矩阵不同的位置表示的RGBA值,其范围在0.0F至2.0F之间,1为保持原图的RGB值。每一行的第五列数字表示偏移值,何为偏移值?顾名思义当我们想让颜色更倾向于红色的时候就增大R向量中的偏移值,想让颜色更倾向于蓝色的时候就增大B向量中的偏移值,这是最最朴素的理解,但是事实上色彩偏移的概念是基于白平衡来理解的,什么是白平衡呢?说得简单点就是白色是什么颜色!如果大家是个单反爱好者或者会些PS就会很容易理解这个概念,在单反的设置参数中有个色彩偏移,其定义的就是白平衡的色彩偏移值,就是当你去拍一张照片的时候白色是什么颜色的,在正常情况下白色是(255, 255, 255, 255)但是现实世界中我们是无法找到这样的纯白物体的,所以在我们用单反拍照之前就会拿一个我们认为是白色的物体让相机记录这个物体的颜色作为白色,然后拍摄时整张照片的颜色都会依据这个定义的白色来偏移!而这个我们定义的“白色”(比如:255, 253, 251, 247)和纯白(255, 255, 255, 255)之间的偏移值(0, 2, 4, 8)我们称之为白平衡的色彩偏移。如果你不理解不要紧,自定义控件系列完结后紧接着就是设计色彩基础~~~~在这你就像我开头说的那样朴素地理解下就好。

那么说了这么多,这玩意到底有啥用呢?我们来做个test!还是接着昨天那个圆环,不过我们今天把它改成绘制一个圆并且去掉线程动画的效果因为我们不需要:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.   
  5.     public CustomView(Context context) {  
  6.         this(context, null);  
  7.     }  
  8.   
  9.     public CustomView(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.         mContext = context;  
  12.   
  13.         // 初始化画笔  
  14.         initPaint();  
  15.     }  
  16.   
  17.     /** 
  18.      * 初始化画笔 
  19.      */  
  20.     private void initPaint() {  
  21.         // 实例化画笔并打开抗锯齿  
  22.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  23.   
  24.         /* 
  25.          * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了 
  26.          *  
  27.          * 画笔样式分三种:  
  28.          * 1.Paint.Style.STROKE:描边  
  29.          * 2.Paint.Style.FILL_AND_STROKE:描边并填充 
  30.          * 3.Paint.Style.FILL:填充 
  31.          */  
  32.         mPaint.setStyle(Paint.Style.FILL);  
  33.   
  34.         // 设置画笔颜色为自定义颜色  
  35.         mPaint.setColor(Color.argb(255255128103));  
  36.   
  37.         /* 
  38.          * 设置描边的粗细,单位:像素px 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素 
  39.          */  
  40.         mPaint.setStrokeWidth(10);  
  41.     }  
  42.   
  43.     @Override  
  44.     protected void onDraw(Canvas canvas) {  
  45.         super.onDraw(canvas);  
  46.   
  47.         // 绘制圆形  
  48.         canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2200, mPaint);  
  49.     }  
  50. }  
运行下是一个橙红色的圆~~是不是有点萝卜头国旗帜的感脚?

下面我们为Paint设置一个色彩矩阵:

  1. // 生成色彩矩阵  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         10000,  
  4.         01000,  
  5.         00100,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
再次运行发现没变化啊!!!草!!!是不是感觉被我坑了?如果你真的那么认为我只能说你压根就没认真看上面的文字,我说过什么?值为1时表示什么?表示不改变原色彩的值!!这时我们改变色彩矩阵:
  1. // 生成色彩矩阵  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         0.5F, 0000,  
  4.         00.5F, 000,  
  5.         000.5F, 00,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
再次运行:

是不是明显不一样了?颜色变深了便淳厚了!我们通过色彩矩阵与原色彩的计算得出的色彩就是这样的。那它们是如何计算的呢?其实说白了就是矩阵之间的运算乘积:


矩阵ColorMatrix的一行乘以矩阵MyColor的一列作为矩阵Result的一行,这里MyColor的RGBA值我们需要转换为[0, 1]。那么我们依据此公式来计算下我们得到的RGBA值是否跟我们计算得出来的圆的RGBA值一样:


我们计算得出最后的RGBA值应该为:0.5, 0.25, 0.2, 1;有兴趣的童鞋可以去PS之类的绘图软件里试试看正不正确对不对~~~这里就不演示了!看完这里有朋友又会说了,这玩意有毛线用啊!改个颜色还这么复杂!劳资直接setColor多爽!!没错,你这样想是对的,因为毕竟我们只是一个颜色,可是如果是一张图片呢????一张图片可有还几十万色彩呢!!!你麻痹你跟我说setColor?那么我们换张图片来试试呗!看看是什么样的效果:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.     private Bitmap bitmap;// 位图  
  5.       
  6.     private int x,y;// 位图绘制时左上角的起点坐标  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化画笔  
  17.         initPaint();  
  18.           
  19.         //初始化资源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化画笔 
  25.      */  
  26.     private void initPaint() {  
  27.         // 实例化画笔  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.     }  
  30.       
  31.     /** 
  32.      * 初始化资源 
  33.      */  
  34.     private void initRes(Context context) {  
  35.         // 获取位图  
  36.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  37.           
  38.         /* 
  39.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  40.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  41.          * 屏幕坐标y轴向上偏移位图一半的高度 
  42.          */  
  43.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  44.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  45.     }  
  46.   
  47.     @Override  
  48.     protected void onDraw(Canvas canvas) {  
  49.         super.onDraw(canvas);  
  50.   
  51.         // 绘制位图  
  52.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  53.     }  
  54. }  
如代码所示我们清除了所有的画笔属性设置因为没必要,从资源获取一个Bitmap绘制在画布上:

一张灰常漂亮的风景图,好!现在我们来为我们的画笔添加一个颜色过滤:

  1. // 生成色彩矩阵  
  2. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  3.         0.5F, 0000,  
  4.         00.5F, 000,  
  5.         000.5F, 00,  
  6.         00010,  
  7. });  
  8. mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));  
大家看到还是刚才那个色彩矩阵,运行下看看什么效果呢:

变暗了对吧!没意思,我们来点更刺激的,改下ColorMatrix矩阵:
  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         0.33F, 0.59F, 0.11F, 00,  
  3.         0.33F, 0.59F, 0.11F, 00,  
  4.         0.33F, 0.59F, 0.11F, 00,  
  5.         00010,  
  6. });  

噢!变灰了!还是没意思!继续改:
  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         -10011,  
  3.         0, -1011,  
  4.         00, -111,  
  5.         00010,  
  6. });  

哟呵!!是不是有点类似PS里反相的效果?我们常看到的图片都是RGB的,颠覆一下思维,看看BGR的试试:

  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         00100,  
  3.         01000,  
  4.         10000,  
  5.         00010,  
  6. });  

这样红色的变成了蓝色而蓝色的就变成了红色,继续改:
  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         0.393F, 0.769F, 0.189F, 00,  
  3.         0.349F, 0.686F, 0.168F, 00,  
  4.         0.272F, 0.534F, 0.131F, 00,  
  5.         00010,  
  6. });  

是不是有点类似于老旧照片的感脚?继续:
  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         1.5F, 1.5F, 1.5F, 0, -1,  
  3.         1.5F, 1.5F, 1.5F, 0, -1,  
  4.         1.5F, 1.5F, 1.5F, 0, -1,  
  5.         00010,  
  6. });  

类似去色后高对比度的效果,继续:
  1. ColorMatrix colorMatrix = new ColorMatrix(new float[]{  
  2.         1.438F, -0.122F, -0.016F, 0, -0.03F,  
  3.         -0.062F, 1.378F, -0.016F, 00.05F,  
  4.         -0.062F, -0.122F, 1.483F, 0, -0.02F,  
  5.         00010,  
  6. });  

饱和度对比度加强,好了不演示了……累死我了!截图粘贴上传!!

这些各种各样的图像效果在哪见过?PS?对的!还有各种拍照软件拍摄后的特效处理!大致原理都是这么来的!有人会问爱哥你傻逼么!这么多参数怎么玩!谁记得!而且TMD用参数调颜色?我映像中都是直接在各种绘图软件(比如PS)里拖进度条的!这怎么玩!淡定!如我所说很多时候你压根不需要了解太多原理,只需站在巨人的丁丁上即可,所以稍安勿躁!再下一个系列教程“设计色彩”中爱哥教你玩转色彩并且让设计和开发无缝结合!

ColorMatrixColorFilter和ColorMatrix就是这么个东西,ColorMatrix类里面也提供了一些实在的方法,比如setSaturation(float sat)设置饱和度,而且ColorMatrix每个方法都用了阵列的计算,如果大家感兴趣可以自己去深挖来看不过我是真心不推荐的~~~

下面我们来看看ColorFilter的另一个子类

LightingColorFilter

顾名思义光照颜色过滤,这肯定是跟光照是有关的了~~该类有且只有一个构造方法:

  1. LightingColorFilter (int mul, int add)  
这个方法非常非常地简单!mul全称是colorMultiply意为色彩倍增,而add全称是colorAdd意为色彩添加,这两个值都是16进制的色彩值0xAARRGGBB。这个方法使用也是非常的简单。还是拿上面那张图片来说吧,比如我们想要去掉绿色:
  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.     private Bitmap bitmap;// 位图  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化画笔  
  17.         initPaint();  
  18.   
  19.         // 初始化资源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化画笔 
  25.      */  
  26.     private void initPaint() {  
  27.         // 实例化画笔  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.   
  30.         // 设置颜色过滤  
  31.         mPaint.setColorFilter(new LightingColorFilter(0xFFFF00FF0x00000000));  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化资源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 获取位图  
  39.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  40.   
  41.         /* 
  42.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  43.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  44.          * 屏幕坐标y轴向上偏移位图一半的高度 
  45.          */  
  46.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  47.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  48.     }  
  49.   
  50.     @Override  
  51.     protected void onDraw(Canvas canvas) {  
  52.         super.onDraw(canvas);  
  53.   
  54.         // 绘制位图  
  55.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  56.     }  
  57. }  
运行后你会发现绿色确实是没了但是原来偏绿的部分现在居然成了红色,为毛!敬请关注下一系列设计色彩文章!!哈哈哈!!当LightingColorFilter(0xFFFFFFFF, 0x00000000)的时候原图是不会有任何改变的,如果我们想增加红色的值,那么LightingColorFilter(0xFFFFFFFF, 0x00XX0000)就好,其中XX取值为00至FF。那么这个方法有什么存在的意义呢?存在必定合理,这个方法存在一定是有它可用之处的,前些天有个盆友在群里问点击一个图片如何直接改变它的颜色而不是为他多准备另一张点击效果的图片,这种情况下该方法就派上用场了!如下图一个灰色的星星,我们点击后让它变成黄色

代码如下,注释很清楚我就不再多说了:
  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.     private Bitmap bitmap;// 位图  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.     private boolean isClick;// 用来标识控件是否被点击过  
  8.   
  9.     public CustomView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public CustomView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.         mContext = context;  
  16.   
  17.         // 初始化画笔  
  18.         initPaint();  
  19.   
  20.         // 初始化资源  
  21.         initRes(context);  
  22.   
  23.         setOnClickListener(new OnClickListener() {  
  24.             @Override  
  25.             public void onClick(View v) {  
  26.                 /* 
  27.                  * 判断控件是否被点击过 
  28.                  */  
  29.                 if (isClick) {  
  30.                     // 如果已经被点击了则点击时设置颜色过滤为空还原本色  
  31.                     mPaint.setColorFilter(null);  
  32.                     isClick = false;  
  33.                 } else {  
  34.                     // 如果未被点击则点击时设置颜色过滤后为黄色  
  35.                     mPaint.setColorFilter(new LightingColorFilter(0xFFFFFFFF0X00FFFF00));  
  36.                     isClick = true;  
  37.                 }  
  38.   
  39.                 // 记得重绘  
  40.                 invalidate();  
  41.             }  
  42.         });  
  43.     }  
  44.   
  45.     /** 
  46.      * 初始化画笔 
  47.      */  
  48.     private void initPaint() {  
  49.         // 实例化画笔  
  50.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  51.     }  
  52.   
  53.     /** 
  54.      * 初始化资源 
  55.      */  
  56.     private void initRes(Context context) {  
  57.         // 获取位图  
  58.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a2);  
  59.   
  60.         /* 
  61.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  62.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  63.          * 屏幕坐标y轴向上偏移位图一半的高度 
  64.          */  
  65.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  66.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  67.     }  
  68.   
  69.     @Override  
  70.     protected void onDraw(Canvas canvas) {  
  71.         super.onDraw(canvas);  
  72.   
  73.         // 绘制位图  
  74.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  75.     }  
  76. }  
运行后点击星星即可变成黄色再点击变回灰色,当我们不想要颜色过滤的效果时,setColorFilter(null)并重绘视图即可!那么为什么要叫光照颜色过滤呢?原因很简单,因为它所呈现的效果就像有色光照在物体上染色一样~~~哎,不说这方法了,看下一个也是最后一个ColorFilter的子类。
PorterDuffColorFilter

PorterDuffColorFilter跟LightingColorFilter一样,只有一个构造方法:

  1. PorterDuffColorFilter(int color, PorterDuff.Mode mode)  
这个构造方法也接受两个值,一个是16进制表示的颜色值这个很好理解,而另一个是PorterDuff内部类Mode中的一个常量值,这个值表示混合模式。那么什么是混合模式呢?混合混合必定是有两种东西混才行,第一种就是我们设置的color值而第二种当然就是我们画布上的元素了!,比如这里我们把Color的值设为红色,而模式设为PorterDuff.Mode.DARKEN变暗:
  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.     private Bitmap bitmap;// 位图  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.   
  8.     public CustomView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public CustomView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.         mContext = context;  
  15.   
  16.         // 初始化画笔  
  17.         initPaint();  
  18.   
  19.         // 初始化资源  
  20.         initRes(context);  
  21.     }  
  22.   
  23.     /** 
  24.      * 初始化画笔 
  25.      */  
  26.     private void initPaint() {  
  27.         // 实例化画笔  
  28.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  29.   
  30.         // 设置颜色过滤  
  31.         mPaint.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DARKEN));  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化资源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 获取位图  
  39.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  40.   
  41.         /* 
  42.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  43.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  44.          * 屏幕坐标y轴向上偏移位图一半的高度 
  45.          */  
  46.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  47.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  48.     }  
  49.   
  50.     @Override  
  51.     protected void onDraw(Canvas canvas) {  
  52.         super.onDraw(canvas);  
  53.   
  54.         // 绘制位图  
  55.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  56.     }  
  57. }  
我们尝试在画布上Draw刚才的那张图片看看:

变暗了……也变红了……这就是PorterDuff.Mode.DARKEN模式给我们的效果,当然PorterDuff.Mode还有其他很多的混合模式,大家可以尝试,但是这里要注意一点,PorterDuff.Mode中的模式不仅仅是应用于图像色彩混合,还应用于图形混合,比如PorterDuff.Mode.DST_OUT就表示裁剪混合图,如果我们在PorterDuffColorFilter中强行设置这些图形混合的模式将不会看到任何对应的效果,关于图形混合我们将在下面详解。

为了提升大家的学习兴趣也算是扩展,我跟大家说说PS中的图层混合模式,在PS中图层的混合模式跟我们PorterDuff.Mode提供的其实是差不多的但是更霸气强大,同样我们还是使用上面那张图来说明:


如图所示,Layer 1我们填充了一层红色而Layer 0是我们的图片,这时我们选择混合模式为Darken(默认为Normal)是不是连名字都跟Android的一样:


效果也必须是一样的:


PS的图层混合模式比Android更多更广泛,但两者同名混合模式所产生的效果是一样的,也基于同样的算法原理这里就不多说了。

下面我们来看另一个跟setColorFilter有几分相似的方法。

setXfermode(Xfermode xfermode)

Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的,首先它与set一样没有公开的实现的方法:

  1. public class Xfermode {  
  2.   
  3.     protected void finalize() throws Throwable {  
  4.         try {  
  5.             finalizer(native_instance);  
  6.         } finally {  
  7.             super.finalize();  
  8.         }  
  9.     }  
  10.   
  11.     private static native void finalizer(int native_instance);  
  12.   
  13.     int native_instance;  
  14. }  
同理可得其必然有一定的子类去实现一些方法供我们使用,查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多,主要是是涉及到图像处理的一些知识可能对大家来说会比较难以理解,不过我会尽量以通俗的方式阐述它们的作用,那好先来看看我们的第一个子类
AvoidXfermode

首先我要告诉大家的是这个API因为不支持硬件加速在API 16已经过时了(大家可以在HardwareAccel查看那些方法不支持硬件加速)……如果想在高于API 16的机子上测试这玩意,必须现在应用或手机设置中关闭硬件加速,在应用中我们可以通过在AndroidManifest.xml文件中设置application节点下的android:hardwareAccelerated属性为false来关闭硬件加速:

  1. <application  
  2.     android:allowBackup="true"  
  3.     android:hardwareAccelerated="false"  
  4.     android:icon="@drawable/ic_launcher"  
  5.     android:label="@string/app_name"  
  6.     android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >  
  7.     <activity  
  8.         android:name="com.aigestudio.customviewdemo.activities.MainActivity"  
  9.         android:label="@string/app_name" >  
  10.         <intent-filter>  
  11.             <action android:name="android.intent.action.MAIN" />  
  12.   
  13.             <category android:name="android.intent.category.LAUNCHER" />  
  14.         </intent-filter>  
  15.     </activity>  
  16. </application>  

AvoidXfermode只有一个含参的构造方法AvoidXfermode(int opColor, int tolerance, AvoidXfermode.Mode mode),其具体实现和ColorFilter一样都被封装在C/C++内,它怎么实现我们不管我们只要知道这玩意怎么用就行对吧。AvoidXfermode有三个参数,第一个opColor表示一个16进制的可以带透明通道的颜色值例如0x12345678,第二个参数tolerance表示容差值,那么什么是容差呢?你可以理解为一个可以标识“精确”或“模糊”的东西,待会我们细讲,最后一个参数表示AvoidXfermode的具体模式,其可选值只有两个:AvoidXfermode.Mode.AVOID或者AvoidXfermode.Mode.TARGET,两者的意思也非常简单,我们先来看

AvoidXfermode.Mode.TARGET

在该模式下Android会判断画布上的颜色是否会有跟opColor不一样的颜色,比如我opColor是红色,那么在TARGET模式下就会去判断我们的画布上是否有存在红色的地方,如果有,则把该区域“染”上一层我们画笔定义的颜色,否则不“染”色,而tolerance容差值则表示画布上的像素和我们定义的红色之间的差别该是多少的时候才去“染”的,比如当前画布有一个像素的色值是(200, 20, 13),而我们的红色值为(255, 0, 0),当tolerance容差值为255时,即便(200, 20, 13)并不等于红色值也会被“染”色,容差值越大“染”色范围越广反之则反,空说无凭我们来看看具体的实现和效果:

  1. public class CustomView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Context mContext;// 上下文环境引用  
  4.     private Bitmap bitmap;// 位图  
  5.     private AvoidXfermode avoidXfermode;// AV模式  
  6.   
  7.     private int x, y, w, h;// 位图绘制时左上角的起点坐标  
  8.   
  9.     public CustomView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public CustomView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.         mContext = context;  
  16.   
  17.         // 初始化画笔  
  18.         initPaint();  
  19.   
  20.         // 初始化资源  
  21.         initRes(context);  
  22.     }  
  23.   
  24.     /** 
  25.      * 初始化画笔 
  26.      */  
  27.     private void initPaint() {  
  28.         // 实例化画笔  
  29.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  30.   
  31.         /* 
  32.          * 当画布中有跟0XFFFFFFFF色不一样的地方时候才“染”色 
  33.          */  
  34.         avoidXfermode = new AvoidXfermode(0XFFFFFFFF0, AvoidXfermode.Mode.TARGET);  
  35.     }  
  36.   
  37.     /** 
  38.      * 初始化资源 
  39.      */  
  40.     private void initRes(Context context) {  
  41.         // 获取位图  
  42.         bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a);  
  43.   
  44.         /* 
  45.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  46.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  47.          * 屏幕坐标y轴向上偏移位图一半的高度 
  48.          */  
  49.         x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2;  
  50.         y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2;  
  51.         w = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + bitmap.getWidth() / 2;  
  52.         h = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + bitmap.getHeight() / 2;  
  53.     }  
  54.   
  55.     @Override  
  56.     protected void onDraw(Canvas canvas) {  
  57.         super.onDraw(canvas);  
  58.   
  59.         // 先绘制位图  
  60.         canvas.drawBitmap(bitmap, x, y, mPaint);  
  61.   
  62.         // “染”什么色是由我们自己决定的  
  63.         mPaint.setARGB(25521153243);  
  64.   
  65.         // 设置AV模式  
  66.         mPaint.setXfermode(avoidXfermode);  
  67.   
  68.         // 画一个位图大小一样的矩形  
  69.         canvas.drawRect(x, y, w, h, mPaint);  
  70.     }  
  71. }  
在高于API 16的测试机上会得到一个矩形的色块(API 16+的都类似,改ROM和关闭了硬件加速的除外):

我们再用低于API 16(或高于API 16但关闭了硬件加速)的测试机运行就会得到另一个不同的效果:


大家可以看到,在我们的模式为TARGET容差值为0的时候此时只有当图片中像色颜色值为0XFFFFFFFF的地方才会被染色,而其他地方不会有改变

AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.TARGET):


而当容差值为255的时候只要是跟0XFFFFFFFF有点接近的地方都会被染色

而另外一种模式

AvoidXfermode.Mode.AVOID

则与TARGET恰恰相反,TARGET是我们指定的颜色是否与画布的颜色一样,而AVOID是我们指定的颜色是否与画布不一样,其他的都与TARGET类似

AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.AVOID):


当模式为AVOID容差值为0时,只有当图片中像素颜色值与0XFFFFFFFF完全不一样的地方才会被染色

AvoidXfermode(0XFFFFFFFF, 255, AvoidXfermode.Mode.AVOID):


当容差值为255时,只要与0XFFFFFFFF稍微有点不一样的地方就会被染色

那么这玩意究竟有什么用呢?比如说当我们只想在白色的区域画点东西或者想把白色区域的地方替换为另一张图片的时候就可以采取这种方式!

Xfermode的第二个子类

PixelXorXfermode

与AvoidXfermode一样也在API 16过时了,该类也提供了一个含参的构造方法PixelXorXfermode(int opColor),该类的计算实现很简单,从官方给出的计算公式来看就是:op ^ src ^ dst,像素色值的按位异或运算,如果大家感兴趣,可以自己用一个纯色去尝试,并自己计算异或运算的值是否与得出的颜色值一样,这里我就不讲了,Because it was deprecated and useless。

Xfermode的最后一个子类也是惟一一个没有过时且沿用至今的子类

PorterDuffXfermode

该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),这个PorterDuff.Mode大家看后是否会有些面熟,它跟上面我们讲ColorFilter时候用到的PorterDuff.Mode是一样的!麻雀虽小五脏俱全,虽说构造方法的签名列表里只有一个PorterDuff.Mode的参数,但是它可以实现很多酷毙的图形效果!!而PorterDuffXfermode就是图形混合模式的意思,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff,那PorterDuffXfermode能做些什么呢?我们先来看一张API DEMO里的图片:


这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:


来定义不同的混合效果,这18种模式Android还为我们提供了它们的计算方式比如LIGHTEN的计算方式为[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],其中Sa全称为Source alpha表示源图的Alpha通道;Sc全称为Source color表示源图的颜色;Da全称为Destination alpha表示目标图的Alpha通道;Dc全称为Destination color表示目标图的颜色,细心的朋友会发现“[……]”里分为两部分,其中“,”前的部分为“Sa + Da - Sa*Da”这一部分的值代表计算后的Alpha通道而“,”后的部分为“Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)”这一部分的值代表计算后的颜色值,图形混合后的图片依靠这个矢量来计算ARGB的值,如果大家感兴趣可以查看维基百科中对Alpha合成的解释:http://en.wikipedia.org/wiki/Alpha_compositing。作为一个猿,我们不需要知道复杂的图形学计算但是一定要知道这些模式会为我们提供怎样的效果,当大家看到上面API DEMO给出的效果时一定会觉得PorterDuffXfermode其实就是简单的图形交并集计算,比如重叠的部分删掉或者叠加等等,事实上呢!PorterDuffXfermode的计算绝非是根据于此!上面我们也说了PorterDuffXfermode的计算是要根据具体的Alpha值和RGB值的,既然如此,我们就来看一个比API DEMO稍微复杂的例子来更有力地说明PorterDuffXfermode是如何工作而我们又能用它做些什么,在这个例子中我将用到两个带有Alpha通道的渐变图形Bitmap:


我们将在不同的模式下混合这两个Bitmap来看看这两个渐变色的颜色值在不同的混合模式下究竟发生了什么?先看看我们的测试代码:

  1. @TargetApi(Build.VERSION_CODES.HONEYCOMB)  
  2. public class PorterDuffView extends View {  
  3.     /* 
  4.      * PorterDuff模式常量 
  5.      * 可以在此更改不同的模式测试 
  6.      */  
  7.     private static final PorterDuff.Mode MODE = PorterDuff.Mode.ADD;  
  8.   
  9.     private static final int RECT_SIZE_SMALL = 400;// 左右上方示例渐变正方形的尺寸大小  
  10.     private static final int RECT_SIZE_BIG = 800;// 中间测试渐变正方形的尺寸大小  
  11.   
  12.     private Paint mPaint;// 画笔  
  13.   
  14.     private PorterDuffBO porterDuffBO;// PorterDuffView类的业务对象  
  15.     private PorterDuffXfermode porterDuffXfermode;// 图形混合模式  
  16.   
  17.     private int screenW, screenH;// 屏幕尺寸  
  18.     private int s_l, s_t;// 左上方正方形的原点坐标  
  19.     private int d_l, d_t;// 右上方正方形的原点坐标  
  20.     private int rectX, rectY;// 中间正方形的原点坐标  
  21.   
  22.     public PorterDuffView(Context context, AttributeSet attrs) {  
  23.         super(context, attrs);  
  24.   
  25.         // 实例化画笔并设置抗锯齿  
  26.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  27.   
  28.         // 实例化业务对象  
  29.         porterDuffBO = new PorterDuffBO();  
  30.   
  31.         // 实例化混合模式  
  32.         porterDuffXfermode = new PorterDuffXfermode(MODE);  
  33.   
  34.         // 计算坐标  
  35.         calu(context);  
  36.     }  
  37.   
  38.     /** 
  39.      * 计算坐标 
  40.      *  
  41.      * @param context 
  42.      *            上下文环境引用 
  43.      */  
  44.     private void calu(Context context) {  
  45.         // 获取包含屏幕尺寸的数组  
  46.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  47.   
  48.         // 获取屏幕尺寸  
  49.         screenW = screenSize[0];  
  50.         screenH = screenSize[1];  
  51.   
  52.         // 计算左上方正方形原点坐标  
  53.         s_l = 0;  
  54.         s_t = 0;  
  55.   
  56.         // 计算右上方正方形原点坐标  
  57.         d_l = screenW - RECT_SIZE_SMALL;  
  58.         d_t = 0;  
  59.   
  60.         // 计算中间方正方形原点坐标  
  61.         rectX = screenW / 2 - RECT_SIZE_BIG / 2;  
  62.         rectY = RECT_SIZE_SMALL + (screenH - RECT_SIZE_SMALL) / 2 - RECT_SIZE_BIG / 2;  
  63.     }  
  64.   
  65.     @Override  
  66.     protected void onDraw(Canvas canvas) {  
  67.         super.onDraw(canvas);  
  68.         // 设置画布颜色为黑色以便我们更好地观察  
  69.         canvas.drawColor(Color.BLACK);  
  70.   
  71.         // 设置业务对象尺寸值计算生成左右上方的渐变方形  
  72.         porterDuffBO.setSize(RECT_SIZE_SMALL);  
  73.   
  74.         /* 
  75.          * 画出左右上方两个正方形 
  76.          * 其中左边的的为src右边的为dis 
  77.          */  
  78.         canvas.drawBitmap(porterDuffBO.initSrcBitmap(), s_l, s_t, mPaint);  
  79.         canvas.drawBitmap(porterDuffBO.initDisBitmap(), d_l, d_t, mPaint);  
  80.   
  81.         /* 
  82.          * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me 
  83.          */  
  84.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  85.   
  86.         // 重新设置业务对象尺寸值计算生成中间的渐变方形  
  87.         porterDuffBO.setSize(RECT_SIZE_BIG);  
  88.   
  89.         // 先绘制dis目标图  
  90.         canvas.drawBitmap(porterDuffBO.initDisBitmap(), rectX, rectY, mPaint);  
  91.   
  92.         // 设置混合模式  
  93.         mPaint.setXfermode(porterDuffXfermode);  
  94.   
  95.         // 再绘制src源图  
  96.         canvas.drawBitmap(porterDuffBO.initSrcBitmap(), rectX, rectY, mPaint);  
  97.   
  98.         // 还原混合模式  
  99.         mPaint.setXfermode(null);  
  100.   
  101.         // 还原画布  
  102.         canvas.restoreToCount(sc);  
  103.     }  
  104. }  
代码中我们使用到了View的离屏缓冲,也通俗地称之为层,这个概念很简单,我们在绘图的时候新建一个“层”,所有的绘制操作都在该层上而不影响该层以外的图像,比如代码中我们在绘制了画布颜色和左右上方两个方形后就新建了一个图层来绘制中间的大正方形,这个方形和左右上方的方形是在两个不同的层上的:

注:图中所显示色彩效果与我们的代码不同,上图只为演示图层概念

当我们绘制完成后要通过restore将所有缓冲(层)中的绘制操作还原到画布以结束绘制,具体关于画布的知识在自定义控件其实很简单1/3,这里就不多说了,下面我们看具体各种模式的计算效果

PS:Src为源图像,意为将要绘制的图像;Dis为目标图像,意为我们将要把源图像绘制到的图像……是不是感脚很拗口 = = !Fuck……意会意会~~

PorterDuff.Mode.ADD

计算方式:Saturate(S + D);Chinese:饱和相加


从计算方式和显示的结果我们可以看到,ADD模式简单来说就是对图像饱和度进行相加,这个模式在应用中不常用,我唯一一次使用它是通过代码控制RGB通道的融合生成图片。

PorterDuff.Mode.CLEAR

计算方式:[0, 0];Chinese:清除

清除图像,很好理解不扯了。

PorterDuff.Mode.DARKEN

计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];Chinese:变暗


这个模式计算方式目测很复杂,其实效果很好理解,两个图像混合,较深的颜色总是会覆盖较浅的颜色,如果两者深浅相同则混合,如图,黄色覆盖了红色而蓝色和青色因为是跟透明混合所以不变。细心的朋友会发现青色和黄色之间有一层类似橙色的过渡色,这就是混合的结果。在实际的测试中源图和目标图的DARKEN混合偶尔会有相反的结果比如红色覆盖了黄色,这源于Android对颜色值“深浅”的定义,我暂时没有在官方查到有关资料不知道是否与图形图像学一致。DARKEN模式的应用在图像色彩方面比较广泛我们可以利用其特性来获得不同的成像效果,这点与之前介绍的ColorFilter有点类似。

PorterDuff.Mode.DST

计算方式:[Da, Dc];Chinese:只绘制目标图像


如Chinese所说,很好理解。

PorterDuff.Mode.DST_ATOP

计算方式:[Sa, Sa * Dc + Sc * (1 - Da)];Chinese:在源图像和目标图像相交的地方绘制目标图像而在不相交的地方绘制源图像


PorterDuff.Mode.DST_IN

计算方式:[Sa * Da, Sa * Dc];Chinese:只在源图像和目标图像相交的地方绘制目标图像


最常见的应用就是蒙板绘制,利用源图作为蒙板“抠出”目标图上的图像,这里我讲一个很简单的例子,如果大家用过PS就很容易理解,我这里有两张图:


一张是一个很漂亮的手绘古典美女:


而另一张是一张只有黑色和透明通道的遮罩图:


我们把这张美女图画在我们的屏幕上:

  1. public class DisInView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Bitmap bitmapDis;// 位图  
  4.   
  5.     private int x, y;// 位图绘制时左上角的起点坐标  
  6.     private int screenW, screenH;// 屏幕尺寸  
  7.   
  8.     public DisInView(Context context) {  
  9.         this(context, null);  
  10.     }  
  11.   
  12.     public DisInView(Context context, AttributeSet attrs) {  
  13.         super(context, attrs);  
  14.   
  15.         // 初始化画笔  
  16.         initPaint();  
  17.   
  18.         // 初始化资源  
  19.         initRes(context);  
  20.     }  
  21.   
  22.     /** 
  23.      * 初始化画笔 
  24.      */  
  25.     private void initPaint() {  
  26.         // 实例化画笔  
  27.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  28.     }  
  29.   
  30.     /** 
  31.      * 初始化资源 
  32.      */  
  33.     private void initRes(Context context) {  
  34.         // 获取位图  
  35.         bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  36.   
  37.         // 获取包含屏幕尺寸的数组  
  38.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  39.   
  40.         // 获取屏幕尺寸  
  41.         screenW = screenSize[0];  
  42.         screenH = screenSize[1];  
  43.   
  44.         /* 
  45.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  46.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  47.          * 屏幕坐标y轴向上偏移位图一半的高度 
  48.          */  
  49.         x = screenW / 2 - bitmapDis.getWidth() / 2;  
  50.         y = screenH / 2 - bitmapDis.getHeight() / 2;  
  51.   
  52.     }  
  53.   
  54.     @Override  
  55.     protected void onDraw(Canvas canvas) {  
  56.         super.onDraw(canvas);  
  57.   
  58.         // 绘制美女图  
  59.         canvas.drawBitmap(bitmapDis, x, y, mPaint);  
  60.     }  
  61. }  
运行后如下:

美女脑袋上有个文字标识巨恶心而且因为图片画质问题美图周围还有一片淡黄色的不好看,那我们就通过刚才那个黑色的透明通道图把美女“抠”出来:

  1. public class DisInView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Bitmap bitmapDis, bitmapSrc;// 位图  
  4.     private PorterDuffXfermode porterDuffXfermode;// 图形混合模式  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public DisInView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public DisInView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 实例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);  
  18.   
  19.         // 初始化画笔  
  20.         initPaint();  
  21.   
  22.         // 初始化资源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化画笔 
  28.      */  
  29.     private void initPaint() {  
  30.         // 实例化画笔  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化资源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 获取位图  
  39.         bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  40.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);  
  41.   
  42.         // 获取包含屏幕尺寸的数组  
  43.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  44.   
  45.         // 获取屏幕尺寸  
  46.         screenW = screenSize[0];  
  47.         screenH = screenSize[1];  
  48.   
  49.         /* 
  50.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  51.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  52.          * 屏幕坐标y轴向上偏移位图一半的高度 
  53.          */  
  54.         x = screenW / 2 - bitmapDis.getWidth() / 2;  
  55.         y = screenH / 2 - bitmapDis.getHeight() / 2;  
  56.   
  57.     }  
  58.   
  59.     @Override  
  60.     protected void onDraw(Canvas canvas) {  
  61.         super.onDraw(canvas);  
  62.         canvas.drawColor(Color.WHITE);  
  63.   
  64.         /* 
  65.          * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me 
  66.          */  
  67.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  68.   
  69.         // 先绘制dis目标图  
  70.         canvas.drawBitmap(bitmapDis, x, y, mPaint);  
  71.   
  72.         // 设置混合模式  
  73.         mPaint.setXfermode(porterDuffXfermode);  
  74.   
  75.         // 再绘制src源图  
  76.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  77.   
  78.         // 还原混合模式  
  79.         mPaint.setXfermode(null);  
  80.   
  81.         // 还原画布  
  82.         canvas.restoreToCount(sc);  
  83.     }  
  84. }  
看!只剩米女了~~~~:

当然该混合模式的用法绝不止这么简单,这只是阐述了一个原理,更棒的用法就看你怎么用了~~~~

PorterDuff.Mode.DST_OUT

计算方式:[Da * (1 - Sa), Dc * (1 - Sa)];Chinese:只在源图像和目标图像不相交的地方绘制目标图像

上面那个例子呢我们把米女抠了出来,而这次我们将从一个色块中把米女的轮廓挖出来~~~~啦啦啦:

  1. public class DisOutView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Bitmap bitmapSrc;// 位图  
  4.     private PorterDuffXfermode porterDuffXfermode;// 图形混合模式  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public DisOutView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public DisOutView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 实例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);  
  18.   
  19.         // 初始化画笔  
  20.         initPaint();  
  21.   
  22.         // 初始化资源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化画笔 
  28.      */  
  29.     private void initPaint() {  
  30.         // 实例化画笔  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化资源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 获取位图  
  39.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3_mask);  
  40.   
  41.         // 获取包含屏幕尺寸的数组  
  42.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  43.   
  44.         // 获取屏幕尺寸  
  45.         screenW = screenSize[0];  
  46.         screenH = screenSize[1];  
  47.   
  48.         /* 
  49.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  50.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  51.          * 屏幕坐标y轴向上偏移位图一半的高度 
  52.          */  
  53.         x = screenW / 2 - bitmapSrc.getWidth() / 2;  
  54.         y = screenH / 2 - bitmapSrc.getHeight() / 2;  
  55.   
  56.     }  
  57.   
  58.     @Override  
  59.     protected void onDraw(Canvas canvas) {  
  60.         super.onDraw(canvas);  
  61.         canvas.drawColor(Color.WHITE);  
  62.   
  63.         /* 
  64.          * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me 
  65.          */  
  66.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  67.   
  68.         // 先绘制一层颜色  
  69.         canvas.drawColor(0xFF8f66DA);  
  70.   
  71.         // 设置混合模式  
  72.         mPaint.setXfermode(porterDuffXfermode);  
  73.   
  74.         // 再绘制src源图  
  75.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  76.   
  77.         // 还原混合模式  
  78.         mPaint.setXfermode(null);  
  79.   
  80.         // 还原画布  
  81.         canvas.restoreToCount(sc);  
  82.     }  
  83. }  
看看美女那动人的轮廓~~~~~么么哒:

PorterDuff.Mode.DST_OVER

计算方式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc];Chinese:在源图像的上方绘制目标图像


这个就不说啦,就是两个图片谁在上谁在下的意思

PorterDuff.Mode.LIGHTEN

计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)];Chinese:变亮


与DARKEN相反,不多说了

PorterDuff.Mode.MULTIPLY

计算方式:[Sa * Da, Sc * Dc];Chinese:正片叠底


该模式通俗的计算方式很简单,源图像素颜色值乘以目标图像素颜色值除以255即得混合后图像像素的颜色值,该模式在设计领域应用广泛,因为其特性黑色与任何颜色混合都会得黑色,在手绘的上色、三维动画的UV贴图绘制都有应用,具体效果大家自己尝试我就不说了

PorterDuff.Mode.OVERLAY

计算方式:未给出;Chinese:叠加


这个模式没有在官方的API DEMO中给出,谷歌也没有给出其计算方式,在实际效果中其对亮色和暗色不起作用,也就是说黑白色无效,它会将源色与目标色混合产生一种中间色,这种中间色生成的规律也很简单,如果源色比目标色暗,那么让目标色的颜色倍增否则颜色递减。

PorterDuff.Mode.SCREEN

计算方式:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc];Chinese:滤色


计算方式我不解释了,滤色产生的效果我认为是Android提供的几个色彩混合模式中最好的,它可以让图像焦媃幻化,有一种色调均和的感觉:

  1. public class ScreenView extends View {  
  2.     private Paint mPaint;// 画笔  
  3.     private Bitmap bitmapSrc;// 位图  
  4.     private PorterDuffXfermode porterDuffXfermode;// 图形混合模式  
  5.   
  6.     private int x, y;// 位图绘制时左上角的起点坐标  
  7.     private int screenW, screenH;// 屏幕尺寸  
  8.   
  9.     public ScreenView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     public ScreenView(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.   
  16.         // 实例化混合模式  
  17.         porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
  18.   
  19.         // 初始化画笔  
  20.         initPaint();  
  21.   
  22.         // 初始化资源  
  23.         initRes(context);  
  24.     }  
  25.   
  26.     /** 
  27.      * 初始化画笔 
  28.      */  
  29.     private void initPaint() {  
  30.         // 实例化画笔  
  31.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  32.     }  
  33.   
  34.     /** 
  35.      * 初始化资源 
  36.      */  
  37.     private void initRes(Context context) {  
  38.         // 获取位图  
  39.         bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.a3);  
  40.   
  41.         // 获取包含屏幕尺寸的数组  
  42.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  43.   
  44.         // 获取屏幕尺寸  
  45.         screenW = screenSize[0];  
  46.         screenH = screenSize[1];  
  47.   
  48.         /* 
  49.          * 计算位图绘制时左上角的坐标使其位于屏幕中心 
  50.          * 屏幕坐标x轴向左偏移位图一半的宽度 
  51.          * 屏幕坐标y轴向上偏移位图一半的高度 
  52.          */  
  53.         x = screenW / 2 - bitmapSrc.getWidth() / 2;  
  54.         y = screenH / 2 - bitmapSrc.getHeight() / 2;  
  55.   
  56.     }  
  57.   
  58.     @Override  
  59.     protected void onDraw(Canvas canvas) {  
  60.         super.onDraw(canvas);  
  61.         canvas.drawColor(Color.WHITE);  
  62.   
  63.         /* 
  64.          * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me 
  65.          */  
  66.         int sc = canvas.saveLayer(00, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);  
  67.   
  68.         // 先绘制一层带透明度的颜色  
  69.         canvas.drawColor(0xcc1c093e);  
  70.   
  71.         // 设置混合模式  
  72.         mPaint.setXfermode(porterDuffXfermode);  
  73.   
  74.         // 再绘制src源图  
  75.         canvas.drawBitmap(bitmapSrc, x, y, mPaint);  
  76.   
  77.         // 还原混合模式  
  78.         mPaint.setXfermode(null);  
  79.   
  80.         // 还原画布  
  81.         canvas.restoreToCount(sc);  
  82.     }  
  83. }  
它比原图多一层蓝紫色调给人感觉更古典~~

PorterDuff.Mode.SRC

计算方式:[Sa, Sc];Chinese:显示源图

只绘制源图,SRC类的模式跟DIS的其实差不多就不多说了,大家多动手自己试试,我已经写不动了快………………………………………………………………………………………………………………

PorterDuff.Mode.SRC_ATOP

计算方式:[Da, Sc * Da + (1 - Sa) * Dc];Chinese:在源图像和目标图像相交的地方绘制源图像,在不相交的地方绘制目标图像

PorterDuff.Mode.SRC_IN

计算方式:[Sa * Da, Sc * Da];Chinese:只在源图像和目标图像相交的地方绘制源图像


PorterDuff.Mode.SRC_OUT

计算方式:[Sa * (1 - Da), Sc * (1 - Da)];Chinese:只在源图像和目标图像不相交的地方绘制源图像


PorterDuff.Mode.SRC_OVER

计算方式:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc];Chinese:在目标图像的顶部绘制源图像


PorterDuff.Mode.XOR

计算方式:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc];Chinese:在源图像和目标图像重叠之外的任何地方绘制他们,而在不重叠的地方不绘制任何内容


XOR我们在将PixelXorXfermode的时候提到过,不了解的话上去看看~~~实在写不动了

那么这些混合模式究竟有什么用?能够给我们带来什么好处呢?假设我们要画一个闹钟,如下:


注:该闹钟图标为已投入运行项目文件并已有商标,请大家不要以任何盈利手段为目的盗用,当然做练习是没问题的

构思一下怎么做,我们需要画一个圆圈做钟体,两个Path(Path为路径,我们将会在1/3详细学习到,这里就先follow me)作为指针,问题是两个铃铛~~~~如果我们不会混合模式,一定会想怎么计算坐标啊去绘制曲线然后闭合然后填充啊之类………………实际上有必要吗?这个闹铃不就是一个小圆再用一个大圆去遮罩吗:


问题是不是一下子就变简单了?如果等你去计算怎么画路径怎么闭合曲线填充颜色还有多屏幕的匹配………………哥已经死了又活过来又死了…………在学完1/2的View尺寸计算和布局后我会教大家如何做类似的View并匹配在所有的屏幕上~~~~这里就先缓一缓。大家一定要有这样的思维,当你想要去画一个View的时候一定要想想看这个View的图形是不是可以通过基本的几何图形混合来生成,如果可以,那么恭喜你,使用PorterDuffXfermode的混合模式你可以事半功倍!

PorterDuffXfermode的另一个比较常见的应用就是橡皮檫效果,我们可以通过手指不断地触摸屏幕绘制Path,再以Path作遮罩遮掉填充的色块显示下层的图像:

  1. public class EraserView extends View {  
  2.     private static final int MIN_MOVE_DIS = 5;// 最小的移动距离:如果我们手指在屏幕上的移动距离小于此值则不会绘制  
  3.   
  4.     private Bitmap fgBitmap, bgBitmap;// 前景橡皮擦的Bitmap和背景我们底图的Bitmap  
  5.     private Canvas mCanvas;// 绘制橡皮擦路径的画布  
  6.     private Paint mPaint;// 橡皮檫路径画笔  
  7.     private Path mPath;// 橡皮擦绘制路径  
  8.   
  9.     private int screenW, screenH;// 屏幕宽高  
  10.     private float preX, preY;// 记录上一个触摸事件的位置坐标  
  11.   
  12.     public EraserView(Context context, AttributeSet set) {  
  13.         super(context, set);  
  14.   
  15.         // 计算参数  
  16.         cal(context);  
  17.   
  18.         // 初始化对象  
  19.         init(context);  
  20.     }  
  21.   
  22.     /** 
  23.      * 计算参数 
  24.      *  
  25.      * @param context 
  26.      *            上下文环境引用 
  27.      */  
  28.     private void cal(Context context) {  
  29.         // 获取屏幕尺寸数组  
  30.         int[] screenSize = MeasureUtil.getScreenSize((Activity) context);  
  31.   
  32.         // 获取屏幕宽高  
  33.         screenW = screenSize[0];  
  34.         screenH = screenSize[1];  
  35.     }  
  36.   
  37.     /** 
  38.      * 初始化对象 
  39.      */  
  40.     private void init(Context context) {  
  41.         // 实例化路径对象  
  42.         mPath = new Path();  
  43.   
  44.         // 实例化画笔并开启其抗锯齿和抗抖动  
  45.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
  46.   
  47.         // 设置画笔透明度为0是关键!我们要让绘制的路径是透明的,然后让该路径与前景的底色混合“抠”出绘制路径  
  48.         mPaint.setARGB(12825500);  
  49.   
  50.         // 设置混合模式为DST_IN  
  51.         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));  
  52.   
  53.         // 设置画笔风格为描边  
  54.         mPaint.setStyle(Paint.Style.STROKE);  
  55.   
  56.         // 设置路径结合处样式  
  57.         mPaint.setStrokeJoin(Paint.Join.ROUND);  
  58.   
  59.         // 设置笔触类型  
  60.         mPaint.setStrokeCap(Paint.Cap.ROUND);  
  61.   
  62.         // 设置描边宽度  
  63.         mPaint.setStrokeWidth(50);  
  64.   
  65.         // 生成前景图Bitmap  
  66.         fgBitmap = Bitmap.createBitmap(screenW, screenH, Config.ARGB_8888);  
  67.   
  68.         // 将其注入画布  
  69.         mCanvas = new Canvas(fgBitmap);  
  70.   
  71.         // 绘制画布背景为中性灰  
  72.         mCanvas.drawColor(0xFF808080);  
  73.   
  74.         // 获取背景底图Bitmap  
  75.         bgBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a4);  
  76.   
  77.         // 缩放背景底图Bitmap至屏幕大小  
  78.         bgBitmap = Bitmap.createScaledBitmap(bgBitmap, screenW, screenH, true);  
  79.     }  
  80.   
  81.     @Override  
  82.     protected void onDraw(Canvas canvas) {  
  83.         // 绘制背景  
  84.         canvas.drawBitmap(bgBitmap, 00null);  
  85.   
  86.         // 绘制前景  
  87.         canvas.drawBitmap(fgBitmap, 00null);  
  88.   
  89.         /* 
  90.          * 这里要注意canvas和mCanvas是两个不同的画布对象 
  91.          * 当我们在屏幕上移动手指绘制路径时会把路径通过mCanvas绘制到fgBitmap上 
  92.          * 每当我们手指移动一次均会将路径mPath作为目标图像绘制到mCanvas上,而在上面我们先在mCanvas上绘制了中性灰色 
  93.          * 两者会因为DST_IN模式的计算只显示中性灰,但是因为mPath的透明,计算生成的混合图像也会是透明的 
  94.          * 所以我们会得到“橡皮擦”的效果 
  95.          */  
  96.         mCanvas.drawPath(mPath, mPaint);  
  97.     }  
  98.   
  99.     /** 
  100.      * View的事件将会在7/12详解 
  101.      */  
  102.     @Override  
  103.     public boolean onTouchEvent(MotionEvent event) {  
  104.         /* 
  105.          * 获取当前事件位置坐标 
  106.          */  
  107.         float x = event.getX();  
  108.         float y = event.getY();  
  109.   
  110.         switch (event.getAction()) {  
  111.         case MotionEvent.ACTION_DOWN:// 手指接触屏幕重置路径  
  112.             mPath.reset();  
  113.             mPath.moveTo(x, y);  
  114.             preX = x;  
  115.             preY = y;  
  116.             break;  
  117.         case MotionEvent.ACTION_MOVE:// 手指移动时连接路径  
  118.             float dx = Math.abs(x - preX);  
  119.             float dy = Math.abs(y - preY);  
  120.             if (dx >= MIN_MOVE_DIS || dy >= MIN_MOVE_DIS) {  
  121.                 mPath.quadTo(preX, preY, (x + preX) / 2, (y + preY) / 2);  
  122.                 preX = x;  
  123.                 preY = y;  
  124.             }  
  125.             break;  
  126.         }  
  127.   
  128.         // 重绘视图  
  129.         invalidate();  
  130.         return true;  
  131.     }  
  132. }  
运行效果如下:

啊啊啊啊啊啊啊啊啊啊啊啊~~~~~PorterDuffXfermode算是暂告一段落,大家一定要学会PorterDuffXfermode各个混合模式的使用,这是android图形绘制的重点之一!

再次强调:PorterDuffXfermode是重点~~~~一定要学会如何灵活使用

源码地址:传送门

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页