参考
Xfermode in android - 解释文档和模式部分写得很好
Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解 - 代码实践分析部分值得细看
网上大部分文章都说有3个类可用, 但是实际上仅需要掌握PoterDuffXfermode
, 因为只有它支持硬件加速, 官方的文档的描述中Xfermode
的直接继承类也只有它了, 所以一般只用它.
PoterDuffXfermode
Porter-Duff 操作是 1 组 12 项用于描述数字图像合成的基本手法,包括Clear、Source Only、Destination Only、Source Over、Source In、SourceOut、Source Atop、Destination Over、Destination In、DestinationOut、Destination Atop、XOR。通过组合使用 Porter-Duff 操作,可完成任意 2D图像的合成。Thomas Porter 和 Tom Duff 发表于 1984年原始论文的扫描版本
简单来说就是一种图像合成的理论依据, 规定了合成图像时的像素操作. Android中支持总共18种模式, 就不一一列举了. 看懂文档就行.
文档解释
public enum Mode {
// ...
/** [Sa + (1 - Sa)*Da, Dc + (1 - Da)*Sc] */
DST_OVER (4),
/** [Sa * Da, Sa * Dc] */
DST_IN (6),
// ...以下省略
}
文档中每个模式对应一条公式, 公式中的缩写表示:
SRC = source, 表示即将要画的像素
DST = destination, 表示已经存在的像素
Sa = Source alpha, 透明通道值
Da = Dest alpha
Sc = Source color, 颜色值
Dc = Dst color
[AlphaValue, ColorValue] -> 第一个值为进行像素操作后的透明通道值, 第二个值为操作后的颜色值
举个例子:
DST_IN - [Sa * Da, Sa * Dc]
为了简化分析, 假设透明通道值不是0就是1
主要看颜色值的计算 Sa * Dc, 当Sa = 1的时候, 颜色值就是Dc, 也就是说在准备画的像素的alpha值为1的地方, 直接显示原来的像素, Sa = 0的时候不显示任何颜色, 并且只有在Sa和Da都是1的地方才会显示颜色.
加入透明通道值的分析参考Xfermode in android
看懂文档后我们就可以利用Xfermode做各种图形效果, Xfermode可以做的事情理论上
可完成任意 2D图像的合成
远远不限于实现任意形状的ImageView. 接下来就一步步分析如下实现任意形状的ImageView和我遇到的问题.
实现任意形状ImageView
分析
为了最简化代码, 最好能够复用ImageView
, 而ImageView#onDraw
就是把图片画在屏幕上, 也就是说经过ImageView#onDraw
方法后, 图片像素会变成DST
(已经存在的像素).
所以要实现任意形状最直接的办法应该是根据形状裁剪图片像素, 即显示DST
和SRC
重合的部分的DST
像素, 形状内的像素自然是SRC
(即将要画的像素), 转化成公式应该是Sa * Dc
, 查模式说明文档找到我们需要的模式DST_IN
- [Sa * Da, Sa * Dc]
所以我们的核心代码应该如下
super.onDraw(canvas);// 画图片
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));// 设置Xfermode模式
canvas.drawXX();// 画多边形覆盖在图片上
使用saveLayer
在实际测试的时候会发现, 多边形外的像素会变成黑色(也有可能是白色), 这是因为默认情况画布只有一个图层(就是Photoshop里面的图层概念), 此时的DST
不仅是图片, 还包括图片后面的背景像素, 如果清除了多边形外的像素, 当然背景也会被清除掉了, 而一般情况下我们仅需要处理图片本身, 所以实际使用中通常会使用Canvas#saveLayer
来创建新的透明图层来进行图像合成的操作, 此时背景的像素就不会被纳入DST
中.
核心代码变成
int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);// 新增透明图层
super.onDraw(canvas);// 画图片
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));// 设置Xfermode模式
canvas.drawXX();// 画多边形覆盖在图片上
canvas.restoreToCount(layerId);// 合并图层
使用Bitmap
如果我想制作圆形图片, 那么直接通过Canvas#drawCircle
画圆, 实际运行就会发现结果跟预测的不同, 圆形外的像素并没有消失, 为什么?
这是因为直接通过Canvas#drawXX
方法画图时, SRC
仅是图形内的像素, 例如你画了一个圆, 那么SRC(即将要画的像素)仅是圆内的像素, 也就是说图片与圆不重叠的像素并不会有任何变化, 当然就不会消失了.
所以要想圆形外的像素会消失, 我们要把圆形外的像素也纳入SRC
并且使其透明通道值为0.
所以进行"过滤"操作的时候, 例如DST_IN, 仅显示即将要画的像素一般会先创建一个Bitmap
实例, 并先在Bitmap
画要保留的图形, 然后再把Bitmap
画在图片上, 此时SRC
的像素包括了整个Bitmap
而不仅仅是图形内的像素
.假设我们要制作的是菱形的图片, 那么代码就变成
// 创建一个跟图片一样大小的Bitmap, 并画一个旋转了45度的正方形
private Bitmap createMask() {
int maskWidth = getMeasuredWidth();
int maskHeight = getMeasuredHeight();
Bitmap mask = Bitmap.createBitmap(maskWidth, maskHeight, Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(mask);
canvas.translate(maskWidth / 2, 0);
canvas.rotate(45);
int rectSize = (int) (maskWidth / 2 / Math.sin(Math.toRadians(45)));
canvas.drawRect(0, 0, rectSize, rectSize, mPaint);
}
// 核心操作
int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);// 新增透明图层
super.onDraw(canvas);// 画图片
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));// 设置Xfermode模式
canvas.drawBitmap(createMask(), 0, 0, mPaint);// 画多边形覆盖在图片上
canvas.restoreToCount(layerId);
这里有个小技巧, 在创建Bitmap
的时候使用了Bitmap.Config.ALPHA_8
, 这是因为DST_IN
的公式中仅使用了Sa
, 不需要有颜色, 所以只使用ALPHA_8
就足够了, 可以节省内存.
效果图
完整的实现旋转45度正方形ImageView
代码
注: 在边长计算中假设了ImageView
本身是一个正方形