Android高级进阶——绘图篇(六)setXfermode混合
开篇
本篇我们将使用不同模式来实现一些常见效果,具体看下文吧
示例1、区域波纹
这种效果是不是看着挺炫的?复杂么?NO! 非常简单,首先你需要去考虑采用什么方式可以实现这种效果的展示,一般出现这种情况首先考虑的就是采用混合模式(setXfermode方法是在混合的模式实现),那么混合模式那么多,要具体使用哪一个呢?
其实上篇博客我们介绍了 最常用的四种,SRC_IN、DST_IN、SRC_OUT、DST_OUT 都可以实现这种效果,只不过前提条件不一样罢了
-
使用 DST_IN 模式
我们知道使用 DST_IN 的前提是不能与空白像素(透明像素)相交,一旦产生相交区域,那么相交区域也会变成空白像素,我们就可以利用这个特点来实现这个效果,而如果要想实现这个效果首先需要考虑把谁作为目标图像,把谁作为源图像,这里目标图像肯定是 动态的贝塞尔曲线 了(因为相交区域显示颜色是贝塞尔曲线的颜色),源图像就是 文本信息了: -
1、动态贝塞尔曲线 绘制
这个前面介绍过 《Android高级绘制——绘图篇(三)路径Path绘制以及贝塞尔曲线使用技巧》 ,这里只是改了显示区域罢了
private void init() {
//初始化画笔
paint = new Paint();
//设置画笔颜色(不能为完全透明)
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
// 源图像
srcBitmap = Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
//目标图像
dstBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
//路径(贝塞尔曲线)
path = new Path();
//绘制中奖信息文字的画笔
textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
textPaint.setTextSize(150);
Canvas srcCanvas = new Canvas(srcBitmap);
text = “aKaic’Blog”;
//获取文字宽度
textWidth = textPaint.measureText(text);
//贝塞尔曲线波长
mItemWaveLength = textWidth;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
//使用离屏绘制
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
path.reset();
//这里的贝塞尔曲线的坐标为(文字开始绘制的X坐标,文字基线Y),文字的绘制是在源图像的中间绘制的(水平,垂直居中显示)当然垂直上并没有真正的居中,需要的话可以根据 paint.getFontMetrics(); 进行计算
path.moveTo(-mItemWaveLength + dx, srcBitmap.getHeight() / 2 - 55);
//这里和之前一样,通过 rQuadTO 方法实现
float halfWavelen = mItemWaveLength / 2;
for (float i = -mItemWaveLength; i <= textWidth + mItemWaveLength; i += mItemWaveLength) {
path.rQuadTo(halfWavelen / 2, 50, halfWavelen, 0);
path.rQuadTo(halfWavelen / 2, -50, halfWavelen, 0);
}
path.lineTo(srcBitmap.getWidth(), srcBitmap.getHeight());
path.lineTo(0, srcBitmap.getHeight());
path.close();
//先将路径绘制到 bitmap上
Canvas dstCanvas = new Canvas(dstBitmap);
//擦除 dstCanvas 这个画布上的信息(这个很重要)
dstCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
dstCanvas.drawPath(path, paint);
//绘制 目标图像
canvas.drawBitmap(dstBitmap, 100, 100, paint);
canvas.restoreToCount(layerID);
}
public void startAnim() {
ValueAnimator animator = ValueAnimator.ofFloat(0, mItemWaveLength);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
效果图如下:
Jietu20180424-150354.gif需要注意的地方
//擦除 dstCanvas 这个画布上的信息
dstCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
这行代码非常重要,因为我们把路径往 bitmap 上绘制,所以如果没有清除之前绘制的内容,会变成什么样呢? 来看一下:
Jietu20180424-152658.gif其实就是重叠导致的,加上上面那行代码就OK了
- 2、绘制文字
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Canvas srcCanvas = new Canvas(srcBitmap);
//居中绘制文字,这里没有考虑高度居中
srcCanvas.drawText(text, (srcBitmap.getWidth() - textWidth) / 2, srcBitmap.getHeight() / 2, textPaint);
//使用离屏绘制
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
path.reset();
path.moveTo(-mItemWaveLength + dx, srcBitmap.getHeight() / 2 - 55);
float halfWavelen = mItemWaveLength / 2;
for (float i = -mItemWaveLength; i <= textWidth + mItemWaveLength; i += mItemWaveLength) {
path.rQuadTo(halfWavelen / 2, 50, halfWavelen, 0);
path.rQuadTo(halfWavelen / 2, -50, halfWavelen, 0);
}
path.lineTo(srcBitmap.getWidth(), srcBitmap.getHeight());
path.lineTo(0, srcBitmap.getHeight());
path.close();
//先将路径绘制到 bitmap上
Canvas dstCanvas = new Canvas(dstBitmap);
dstCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
dstCanvas.drawPath(path, paint);
canvas.drawBitmap(dstBitmap, 100, 100, paint);
//绘制 目标图像
canvas.drawBitmap(srcBitmap, 100, 100, paint);
canvas.restoreToCount(layerID);
}
需要注意的地方:
-
因为相交区域其实就是“aKaic’Blog”,所以在绘制文字时画笔 style 必须采用 FILL_AND_STROKE 或者 FILL
-
3、设置混合模式为 DST_IN
canvas.drawBitmap(dstBitmap, 100, 100, paint);
//设置 模式 为 DST_IN
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//绘制 目标图像
canvas.drawBitmap(srcBitmap, 100, 100, paint);
效果图如下:
为什么设置了混合模式之后,反而变成了这种效果呢? 别误会,其实出现这个效果是对的,不出现这个效果反而才不正常了,因为我们这里设置的混合模式为 DST_IN,所以源图像 与 目标图像 相交区域的显示是正确的,那怎么解决上面的问题呢?其实很简单,我们只需要在把文本绘制一遍就可以了,但是要注意绘制的地方
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Canvas srcCanvas = new Canvas(srcBitmap);
//居中绘制文字,这里没有考虑高度居中
srcCanvas.drawText(text, (srcBitmap.getWidth() - textWidth) / 2, srcBitmap.getHeight() / 2, textPaint);
//绘制文本 第一遍
canvas.drawBitmap( rcBitmap, 100, 100, paint);
//使用离屏绘制
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
…省略部分代码
canvas.drawBitmap(dstBitmap, 100, 100, paint);
//设置 模式 为 DST_IN
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//绘制 目标图像 第二遍
canvas.drawBitmap(srcBitmap, 100, 100, paint);
canvas.restoreToCount(layerID);
}
上面是使用 DST_IN 实现的,那么如果我们要使用 SRC_IN 实现
- 使用 SRC_IN 实现
想要使用 SRC_IN 实现上面效果,其实很简单,因为 SRC_IN 和 DST_IN 它们原理是一样的,所以我们只需要把混合模式改为 SRC_IN,然后绘制的时候把 文字 作为 目标图像,动态贝塞尔曲线 作为 源图像就可以实现
代码这里就不给了,很简单
示例2、心电图
下面我们再来实现一个心电图的动画,效果图如下
很明显,正规的心电图应该是利用Path把当前的实时的点连接起来,我这里只是一张图片(hearmap.png)通过使用动画来实现的
image.png中间是一条心电图线,其余位置都是透明像素;大家先想想我们要怎么利用这张图片实现上面的动画呢?
方法有很多种,DST_IN、SRC_IN、SRC_OUT、DST_OUT 都可以实现,这里只给出一种实现方式,其他方式自己动手试试吧:
- 使用 DST_IN 实现:
还是根据相交区域有空白像素会变为空白像素(透明)这个规则来实现,很明显,在使用 DST_IN 时,相交的区域是不能够出现 空白像素的,不然效果就拜拜了,但是不相交的区域可以通过填充空白像素来隐藏你不想让它显示的内容,具体实现如下:
private void init() {
//初始化画笔
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();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//创建源图像的 Canvas
Canvas c = new Canvas(BmpSRC);
//清空bitmap
c.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
//画上矩形 是由 0 变 1 的过程,画出的矩形是越来越大的,且矩形左上角的顶点距离屏幕左边是越来越近的,自己理解吧,感觉越说越乱了
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);
}
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();
}
}
它这个实现其实很好理解,画个图吧:
xindiantu.png因为 黑色矩形 是一点点加大的,所以只有 黑色矩形 和 目标图像 相交的区域 心电图 才会显示出来,当 黑色矩形 变得和 目标图像 一样大 时,心电图就会被完全显示;
-
使用 SRC_IN 实现 可以采用同一个逻辑实现,只需要换一下绘制的先后顺序就OK
-
使用 SRC_OUT 或者 DST_OUT
这两个模式我并没有测试,但是如果实现的话可以这样去实现,使用这两种模式时其实 和 SRC_IN 和 DST_IN 刚好相反,使用 SRC_IN/DST_IN 是 黑色矩形 是一个 递增 的过程,在使用 SRC_OUT/DST_OUT 时我们只需要反过来就行 直接绘制一个和目标图像同等大小的黑色矩形,然后慢慢减小黑色矩形(因为使用 SRC_OUT/DST_OUT时,当目标图像和 源图像相交区域都没有空白像素时,相交区域会变成空白像素)利用这个规则就可以轻松实现
3、不规则波纹
上面我们实现的波纹效果都是规则的,如果我们想实现如下图这样的不规则波纹要怎么办呢?
Jietu20180424-171018.gif在这里我们需要用到两张图:
一张圆形遮罩(circle_shape.png)
一张不规则的波浪图
想必到这里,可能很多同学都知道要怎么做了
就是在圆形遮罩上绘制不断移动的不规则的波浪图。
代码如下:
private void init() {
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);
}
public void startAnim(){
ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
animator.setDuration(4000);
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();
}
}