图像合成Xfermode和任意形状ImageView

参考
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(已经存在的像素).

所以要实现任意形状最直接的办法应该是根据形状裁剪图片像素, 即显示DSTSRC重合的部分的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就足够了, 可以节省内存.

效果图


图像合成Xfermode和任意形状ImageView_第1张图片
菱形图片效果图.png

完整的实现旋转45度正方形ImageView代码
注: 在边长计算中假设了ImageView本身是一个正方形

你可能感兴趣的:(图像合成Xfermode和任意形状ImageView)