1.前言
上篇介绍了Paint基础以及shader的使用,传送门在这里。剩下的高级应用就是xfermode以及滤镜效果。话不多说,咱们发车。
2. Xfermode简介
Xfermode这名字一听就各种酷炫拽爆吊炸天,而所谓Xfermode,其实就是将绘制的图形的像素和Canvas上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到Canvas中形成最终的图形。
用人话说,就是把2张图变成1张图,这里面一共就三个元素,图1、图2以及混合模式,是不是感觉很简单?
我们先来看看Xfermode的类层次图,记住这个叫PorterDuffXfermode的类,Porter和Duff是两个大佬的名字,这个类就是他们整出来的。我们在使用时要时刻记住大佬们光辉的身影,下面还会接着遇到他们。
接着来理解一下元素,图1图2 没什么好说的,重点在于混合模式
看到图以后不要怂,其实也就4*4=16种嘛,还是有规律可寻的。我们知道,一个像素的颜色都是由四个分量组成,即ARGB,A表示的是Alpha值,RGB表示的是颜色。
S是原像素,用[Sa,Sc]表示,Sa表示的就是源像素的Alpha值,Sc表示源像素的颜色值;
D是目标像素,用[Da,Dc]表示,Da表示的就是目标像素的Alpha值,Dc表示目标像素的颜色值。
上图中,蓝色矩形表示的是原图片,黄色圆表示的是目标图片。下面我们进行具体的介绍。
3. Xfermode混合模式
3.1 SRC
SRC[Sa, Sc]:在处理图片相交区域时,总是显示源图片。
3.1.1 SRC_IN [Sa * Da, Sc * Da]
处理图片相交区域时,受到目标图片的Alpha值影响
当我们的目标图片为空白像素的时候,源图片也会变成空白,简单的来说就是用目标图片的透明度来改变源图片的透明度和饱和度,当目标图片的透明度为0时,源图片就不会显示。
3.1.1.1 圆形头像
我们来看一个例子,还是圆形头像,由于代码比较短,直接全部展示出来
public class RoundImageView extends View {
private Bitmap mSrcBitmap;
private Bitmap mDstBitmap;
private Paint mPaint;
public RoundImageView(Context context) {
super(context);
//有些xfermode不支持硬件加速,因此要关闭
setLayerType(View.LAYER_TYPE_SOFTWARE,null);
mSrcBitmap= BitmapFactory.decodeResource(getResources(), R.drawable.xyjy6);
mDstBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.shade);
mPaint=new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先画目标图片
canvas.drawBitmap(mDstBitmap,0,0,mPaint);
//设置xfermode
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//再画原图片
canvas.drawBitmap(mSrcBitmap,0,0,mPaint);
mPaint.setXfermode(null);
}
}
代码很简单不多解释,SRC就是正常的矩形图片,现在来看看DST这张图片资源
最后上效果图
3.1.1.2 图像倒影
不过瘾,再来一个例子,倒影图片,首先是通过Matrix生成倒影图片
Matrix matrix = new Matrix();
matrix.setScale(1F, -1F);
// 生成倒影图
BmpRevert = Bitmap.createBitmap(BmpSRC,0,0,BmpSRC.getWidth(),BmpDST.getHeight(),matrix,true);
接着在onDraw()
中进行xfermode操作
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先画出原图
canvas.drawBitmap(BmpSRC,0,0,mBitPaint);
//再画出倒影
canvas.translate(0,BmpSRC.getHeight());
canvas.drawBitmap(BmpDST,0,0,mBitPaint);
mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(BmpRevert,0,0,mBitPaint);
mBitPaint.setXfermode(null);
}
还是来看下DST图片资源,是一个从上到下逐渐变不透明的图片
最后是效果图
3.1.2 SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
同SRC_IN类似,只是使用了(1 - Da)
用目标图片的透明度的补值来改变源图片的透明度和饱和度,当目标图片的透明度为不透明时,源图片就不会显示
3.1.2.1 橡皮擦效果
那么这位兄弟又能干什么呢?我们先来撸一个橡皮擦效果。
首先是构造方法,这里构造出来的DST就是一张和SRC大小相同的透明图片,由于DST是透明的,Da=0,所以SRC图片可以完整的显示出来
public EraserView(Context context) {
super(context);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mBitPaint = new Paint();
mBitPaint.setColor(Color.BLACK);
mBitPaint.setStyle(Paint.Style.STROKE);
mBitPaint.setStrokeWidth(45);
BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.xyjy6,null);
BmpDST = Bitmap.createBitmap(BmpSRC.getWidth(), BmpSRC.getHeight(), Bitmap.Config.ARGB_8888);
mPath = new Path();
}
接着重写onTouch()
方法,用来响应手势滑动。Path.quadTo()
一般是用来绘制贝塞尔曲线的,之后我们单独拿出来讲,现在就将他理解为一种圆滑的曲线即可。
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(),event.getY());
mPreX = event.getX();
mPreY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
float endX = (mPreX+event.getX())/2;
float endY = (mPreY+event.getY())/2;
mPath.quadTo(mPreX,mPreY,endX,endY);
mPreX = event.getX();
mPreY =event.getY();
break;
case MotionEvent.ACTION_UP:
break;
}
postInvalidate();
return super.onTouchEvent(event);
}
然后重写绘制方法,首先,将手指轨迹画在透明的DST图片上,此时DST图片中手指滑动的部分的Da就变为了1,也就是说,此时SRC这部分的内容Sa就为0,从而实现了擦出效果。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先把手指轨迹画到目标Bitmap上
Canvas c = new Canvas(BmpDST);
c.drawPath(mPath,mBitPaint);
//然后把目标图像画到画布上
canvas.drawBitmap(BmpDST,0,0,mBitPaint);
//计算源图像区域
mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawBitmap(BmpSRC,0,0,mBitPaint);
mBitPaint.setXfermode(null);
}
来看效果图
3.1.2.2 刮刮卡
下面再来个从戒赌吧学来的招式,刮刮卡效果
总体来说和上面的橡皮擦差不多,不过最底层多了一张戒赌的图片
BmpText = BitmapFactory.decodeResource(getResources(), R.drawable.guaguaka_text1,null);
在绘制时要先将这张图绘制出来
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(BmpText,0,0,mBitPaint);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//先把手指轨迹画到目标Bitmap上
Canvas c = new Canvas(BmpDST);
c.drawPath(mPath,mBitPaint);
//然后把目标图像画到画布上
canvas.drawBitmap(BmpDST,0,0,mBitPaint);
//计算源图像区域
mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawBitmap(BmpSRC,0,0,mBitPaint);
mBitPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
值得注意的是图层的关系,要使用到saveLayer
与restoreToCount
方法,关于Canavas的使用,之后会详细介绍。
最后展示效果图
3.1.3 SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
当透明度为100%和0%时,SRC_IN 和 SRC_ATOP是通用的;当透明度不为上述的两个值时,SRC_ATOP 比 SRC_IN 源图像的饱和度会增加,变得更亮一些。
SRC_ATOP与SRC_IN是亲兄弟,SRC_IN能实现的SRC_ATOP基本都能实现。这里就不再介绍了。
3.2 DST
3.2.1 DST_IN [Sa * Da, Sa * Dc]
DST_IN与SRC_IN相反,在相交的时候以源图片的透明度来改变目标图片的透明度和饱和度,当源图片的透明度为0的时候,目标图片完全不显示。
聪明的同学看到这里,就会发现DST_IN也能实现圆形图片和倒影效果,我们不多说,来看点新鲜的。
3.2.1.1 心电图效果
有了之前的基础,这儿就直接上代码来看吧,国际惯例,先上构造方法
public HeartMap(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.heartmap,null);
BmpSRC = Bitmap.createBitmap(BmpDST.getWidth(), BmpDST.getHeight(), Bitmap.Config.ARGB_8888);
mItemWaveLength = BmpDST.getWidth();
startAnim();
}
是不是发现DST与SRC反了一下?接着看看这个改变mItemWaveLength的属性动画
public void startAnim(){
ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(6000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
绘制的套路和之前的橡皮擦、刮刮卡类似
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Canvas c = new Canvas(BmpSRC);
//清空bitmap
c.drawColor(Color.RED, PorterDuff.Mode.CLEAR);
//画上矩形
c.drawRect(BmpDST.getWidth() - dx,0,BmpDST.getWidth(),BmpDST.getHeight(),mPaint);
//模式合成
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(BmpDST,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(BmpSRC,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
来看效果图,理论上是一张动图,大家继续脑补就行了
3.2.1.2 波浪图
以此类推,一些平时看上去高大上的动态效果,比如波浪图,只需要让美工准备一条完整的波浪,我们就可以通过xfermode+属性动画完美实现了。
先看看这条完整的波浪
构造方法如下
public IrregularWaveView(Context context) {
super(context);
mPaint = new Paint();
BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.wave_bg,null);
BmpSRC = BitmapFactory.decodeResource(getResources(),R.drawable.circle_shape,null);
mItemWaveLength = BmpDST.getWidth();
startAnim();
}
动画效果与心电图一样,就不展示了,直接看绘制方法,套路还是一样的
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先画上圆形
canvas.drawBitmap(BmpSRC,0,0,mPaint);
//再画上结果
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(BmpDST,new Rect(dx,0,dx+BmpSRC.getWidth(),BmpSRC.getHeight()),new Rect(0,0,BmpSRC.getWidth(),BmpSRC.getHeight()),mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(BmpSRC,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
来看看效果图
Xfermode就介绍到这里,剩下的类型大伙儿自己去看呗。
4.滤镜
终于来到最符合标题的部分了!给女朋友化妆,你想学的都在这里!
为什么我要说上面这句话呢,第一是为了摆脱标题党这个标签,第二是要给大家大气,因为咱们得先把矩阵的知识给复习一遍!
4.1矩阵
4.1.1 矩阵基础
既然是手把手,自然要从矩阵的定义开始
接着看我们需要用到的矩阵乘法
解释一波,将第一个矩阵A的第一行,与第二个矩阵B的第一列的数字分别相乘,得到的结果相加,最终的值做为结果矩阵的第(1,1)位置的值(即第一行,第一列)。
同样,A矩阵的第一行与B矩阵的第二列的数字分别相乘然后相加,结果做为结果矩阵第(1,2)位置的值(即第一行第二列)。
注意,矩阵A乘以矩阵B和矩阵B乘以矩阵A的结果是不一样的。
4.1.2 颜色矩阵
理解了上面的知识点后,就来看看我们的色彩信息的矩阵表示,首先是理论上的四阶表示法
此时如果想将色彩(0,255,0,255)更改为半透明时,可以使用下面的的矩阵运算来表示:
但是,真实情况下我们所使用的都是五阶矩阵,为什么呢?考虑下面这个变换:
1、红色分量值更改为原来的2倍;
2、绿色分量增加100;
则使用4阶矩阵的乘法无法实现,所以,应该在四阶色彩变换矩阵上增加一个“哑元坐标”,来实现所列的矩阵运算:
4.2 Alpha滤镜
滤镜滤镜,从字面上看就是对镜头的过滤。那么它要过滤些什么东西呢?我们知道,当颜色值为32位的int值时,可以用ARGB来表示 ,其中A表示Alpha值,RGB表示颜色值。滤镜也就是用来针对ARGB的 。
我们从Alpha滤镜开始说起,Android中有一个叫MaskFilter的类,这玩意儿就是用来对Alpha进行过滤的,来看看他的层级
可见MaskFilter有两个子类,其中BlurMaskFilter用来绘制模糊阴影,EmbossMaskFilter用来实现浮雕效果
4.2.1 BlurMaskFilter
首先介绍BlurMaskFilter。在使用时,要设置它的类型,如下
NORMOL -- 整个图像都被模糊掉
SOLID -- 图像边界外产生一层与Paint颜色一致阴影效果,不影响图像的本身
OUTER -- 图像边界外产生一层阴影,并且将图像变成透明效果
INNER -- 在图像内部边沿产生模糊效果
上一个demo,代码相当简单
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setLayerType(View.LAYER_TYPE_SOFTWARE,null);
mPaint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
canvas.drawBitmap(mBitmap,100,100,mPaint);
}
其中类型为BlurMaskFilter.Blur.NORMAL
,来看看效果图
BlurMaskFilter是大佬封装好的类,底层的原理就是对透明度进行过滤。剩下3个类型大家自己去玩玩吧。说不定你女朋友对其中某种特别青睐呢?
4.2.2 EmbossMaskFilter
EmbossMaskFilter就没有什么类型了,不过它的参数比较复杂,需要解释一波
* @param direction 指定光源的位置,长度为xxx的数组标量[x,y,z]
* @param ambient 环境光的因子 (0~1),越接近0,环境光越暗
* @param specular 镜面反射系数 越接近0,镜面反射越强
* @param blurRadius 模糊半径 值越大,模糊效果越明显
每个字都认识,连起来就不晓得再说什么鬼了。由于数学不太好,我也不知道这兄弟有什么用。使用的代码与之前一样,这些参数可以瞎填。
mPaint.setMaskFilter(new EmbossMaskFilter(new float[]{1,1,1},0.3f,60,80));
效果图上可以略微看到一个类似相框的作用。
4.3 Color滤镜
压轴戏终于登场啦,颜色滤镜涉及到前面所说的矩阵,我们要做的就是DIY各种各样的矩阵来对图片进行特效处理。
在使用时,还是通过paint来设置矩阵。
mPaint.setColorFilter(getColorMatrix());
4.3.1 平移运算
getColorMatrix()
用来返回各种不同的矩阵。咱们先来个平移运算当开胃菜,实现一个原谅妆
private ColorFilter getColorMatrix() {
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0,0,0,0,
0,1,0,0,100,
0,0,1,0,0,
0,0,0,1,0,
});
return new ColorMatrixColorFilter(colorMatrix);
}
原谅妆的原理是平移运算,通过加法改变颜色的值,效果如下
4.3.2 反相效果
反向可以实现照片底片的效果,矩阵如下
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
-1, 0,0,0,255,
0,-1,0,0,255,
0,0,-1,0,255,
0,0,0,1,0,
});
效果图很有意思
4.3.3 缩放运算
下面这个矩阵牛逼了,磨脸美白全靠它,可以完美实现颜色增强的效果。
其原理就是乘法运算,如下
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1.2f, 0,0,0,0,
0,1.2f,0,0,0,
0,0,1.2f,0,0,
0,0,0,1.2f,0,
});
效果图是人见人爱的,大家可以往上翻翻看看和原图的对比
4.3.3 黑白照片
黑白照片的去色原理是这样的:只要把R G B 三通道的色彩信息设置成一样,那么图像就会变成灰色,同时为了保证图像亮度不变,同一个通道里的R+G+B =1
不要问为什么,我哪知道为什么。至于如何组合,前辈们早已得出了较好的结论:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0.213f, 0.715f,0.072f,0,0,
0.213f, 0.715f,0.072f,0,0,
0.213f, 0.715f,0.072f,0,0,
0,0,0,1,0,
});
这个妆不太吉利
4.3.4 发色效果
所谓发色效果,就是将颜色进行交换,这个也很好实现,比如红绿交换
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
0,1,0,0,0,
1,0,0,0,0,
0,0,1,0,0,
0,0,0,1,0,
});
看看这绿唇多么性感
4.3.5 复古效果
下面这个顾名思义,给人一种老旧照片的冲击,至于原理么,也是前辈是辛勤探索的结果
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1/2f,1/2f,1/2f,0,0,
1/3f, 1/3f,1/3f,0,0,
1/4f,1/4f,1/4f,0,0,
0,0,0,1,0,
});
有木有盗墓笔记西沙的感觉?
4.3.6 颜色通道
颜色通道就和当年做物理实验的滤波器一样,只允许特定的颜色展示出来,比如这里我们就只允许红色通过
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,1,0,
});
效果图很革命
4.3.7 其他
除了手动定义矩阵以外,我们还可以通过一些Google封装好的方法来实现更复杂的效果。
ColorMatrix拥有setScale()
、setSaturation()
、setRotate()
、setConcat()
等神奇的方法;而ColorFilter还拥有三种实现类。他们都可以尽情的让你去调教。
不过在此我就不再深入了,再下去就是图像、数学那块的内容,术业有专攻啊。
5.总结
至此,Paint的用法都介绍完毕了!我们在学习高级UI的道路上又前进了一大步。
相信大家的收获一定不少,很多人都掌握了几手给女朋友化妆的绝技。
但是首先……
完结撒花~