最近需要做类似于QQ裁剪头像的功能:
以前的我,肯定会糊里糊涂的copy网上的资料完事!!这样完全学不到东西,所以这次认真点,学习里面的“精髓”,虽然不难!
好了,这里会接触到Canvas的Xfermode与Layer的东西,在自定义View中可以实现很复杂的效果。
Xfermode
先稍微了解一下关于Xfermode,Xfermode称为图像混合模式,可以将不同的图像进行混合处理。
Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,由于AvoidXfermode, PixelXorXfermode都已经被标注为过时了,所以这次主要研究的是仍然在使用的PorterDuffXfermode。
PorterDuffXfermode混合的效果可以有:
效果说明:
- CLEAR:清除图像
- SRC:只显示源图像
- DST:只显示目标图像
- SRC_OVER:将源图像放在目标图像上方
- DST_OVER:将目标图像放在源图像上方
- SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】
- DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响
- SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤
- DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤
- SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响
- DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响
- XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制
- DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合
- LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关
- MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值
- SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖
关于Xfermode的了解先到这个。
使用Xfermode
先简单写一个自定义的View,熟悉一下Canvas:
public class Preview extends View {
private Paint paint;
public Preview(Context context) {
this(context, null);
}
public Preview(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public Preview(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画透明层
// 注意:一定要先设置颜色,然后再设置透明度才会起效果
paint.setColor(Color.BLACK);
// 透明度 0(完全透明)- 255(完全不透明)
paint.setAlpha(150);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
// 画圆圈
int radius = getWidth() / 3;
paint.setColor(Color.WHITE);
paint.setAlpha(255);
paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius + 3, paint);
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_orange_light"
/>
<com.johan.magiceye.Preview
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
效果如图:
OK,我们初步写了一个View,画了一个透明层,然后画了一个圆圈,接下来,我们需要把圆圈的透明层去掉。根据Xfermode的混合效果,我们需要的是SRC_OUT模式,我们修改一下onDraw()方法里面的代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画透明层 0 - 255
paint.setColor(Color.BLACK);
paint.setAlpha(150);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
// 画圆圈
int radius = getWidth() / 3;
paint.setColor(Color.WHITE);
paint.setAlpha(255);
paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius + 3, paint);
// 去掉圆圈内的透明层
paint.setStyle(Paint.Style.FILL);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, paint);
paint.setXfermode(null);
}
效果如图:
如图,圆圈的透明层是去掉了,可是为什么会变成黑色呢??这里,你可能就开始懵逼了,为什么会这样呢??
这里我们就要来了解一下Canvas的Layer了。
Canvas是支持图层Layer渲染技术的,Canvas默认就有一个图层Layer,一般情况下,我们使用Canvas的drawXXX方法,都是在这层默认的Layer上面进行的。
所以我们使用SRC_OUT混合模式后,效果其实就是从src(源图像:整个透明层)中抠掉dest(目标图像:实心圆),显示黑色是因为默认Layer背景为黑色(猜测)。
既然Canvas支持多图层,那我们就新建一个图层(新建的图层背景为透明),然后再去drawXXX。
在Canvas中,使用saveLayer方法可以新建一个图层,使用restoreToCount方法把新图层的东西绘制到底部图层(这里是默认图层Layer)上。
好了,我们再来改一下onDraw()方法的代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 新建图层
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 画透明层 0 - 255
paint.setColor(Color.BLACK);
paint.setAlpha(150);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
// 画圆圈
int radius = getWidth() / 3;
paint.setColor(Color.WHITE);
paint.setAlpha(255);
paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius + 3, paint);
// 去掉圆圈内的透明层
paint.setStyle(Paint.Style.FILL);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, paint);
paint.setXfermode(null);
// 恢复图层
canvas.restoreToCount(saveCount);
}
效果如图:
这样就实现了仿QQ头像裁剪的预览图了!
参考
Android Paint Xfermode 学习小结
Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解