背景
项目中遇到需要实现某种 UI 特效的需求,刚好 Xfermode 能解决这个问题。现整理出来记录和备忘。
是什么
- android.graphics.Xfermode 是用于解决自定义 Android 2D 图形渲染管线中「变换模式」问题的基类
- 解决的是两个像素点的混合问题
使用方法
- 以最常见的圆形头像为例,假如我现在已经从一张 JPG/PNG 图片中解码出 android.graphics.Bitmap 位图对象,怎么样实现圆形头像呢?
- 最先想到的方法是「遍历」位图的像素,对不在圆形区域内的像素点设置为透明像素。这种方法是较直观、较基础,不需要考虑具体平台、编程语言
/**
* 对正方形位图做圆形变换处理
* @param src 正方形位图
* @return 圆形变换后的位图
*/
public static Bitmap circle(Bitmap src){
Bitmap result = src;
// 设置有 Alpha 通道
result.setHasAlpha(true);
// 遍历像素
for (int i = 0; i < result.getHeight(); i++) {
for (int j = 0; j < result.getWidth(); j++) {
if (Math.pow(i - result.getHeight()/2, 2) +
Math.pow(j - result.getWidth()/2, 2) >
Math.pow(result.getWidth()/2, 2)){
// 如果不在圆形区域内,则设置为透明
result.setPixel(j, i, Color.TRANSPARENT);
}
}
}
return result;
}
- 除了这个方案之外,Android SDK 也提供了一些 API 来实现对图像的混合计算
/**
* 对正方形位图做圆形变换处理
* @param src 正方形位图
* @return 圆形变换后的位图
*/
public static Bitmap circle(Bitmap src){
int length = src.getWidth();
Bitmap result = Bitmap.createBitmap(length,
length, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(result);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
// 画圆形
c.drawCircle(length>>1, length>>1, length>>1, p);
// 设置 Xfermode 参数
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// 画原始图片
c.drawBitmap(src, 0, 0, p);
return result;
}
原理
- Xfermode 是对两张图片的像素点做混合计算,输出混合后的位图
- 以 android.graphics.PorterDuff.Mode#SRC_IN 这种效果为例,计算公式是:
Dst 是已经存在的位图
Src 是即将要画的位图
alpha 是颜色的 Alpha 值
color 是颜色的 RGB 值
alphaOut = alphaSrc * alphaDst;
colorOut = colorSrc * alphaDst;
- 根据这个公式实现的 API 类似
/**
* 计算两个像素点经过 android.graphics.PorterDuff.Mode#SRC_IN 模式计算<br>
* 系统源码见 http://androidxref.com/8.0.0_r4/xref/external/skia/src/opts/SkXfermode_opts.h#26<br>
* 源像素点的各个分量都乘以目标像素点的 Alpha 分量
*
* @param dst 目标像素点
* @param src 源像素点
* @return 结果像素点
*/
private int srcIn(int dst, int src) {
// 参考 android.graphics.drawable.ColorDrawable.setAlpha() 源码计算 Alpha
// >>> 无符号右移
// >> 有符号右移
int alphaOut = ((src >>> 24) * (dst >>> 24)) >> 8;
int redOut = (((src >> 16) & 0xFF) * (dst >>> 24)) >> 8;
int greenOut = (((src >> 8) & 0xFF) * (dst >>> 24)) >> 8;
int blueOut = (((src >> 0) & 0xFF) * (dst >>> 24)) >> 8;
return ((alphaOut & 0xFF) << 24) | ((redOut & 0xFF) << 16) |
((greenOut & 0xFF) << 8) | ((blueOut & 0xFF) << 0);
}
- 遍历两张图片的所有像素做 SRC_IN 变换
public static void srcIn(Bitmap dst, Bitmap src) {
for (int i = 0; i < dst.getHeight(); i++) {
for (int j = 0; j < dst.getWidth(); j++) {
dst.setPixel(j, i, srcIn(dst.getPixel(j, i), src.getPixel(j, i)));
}
}
}
-
显示结果类似
- 系统实际是怎么计算的呢?以 SOFTWARE 渲染为例
http://androidxref.com/8.0.0_r4/xref/external/skia/src/opts/SkXfermode_opts.h#26
s 是 src 像素点
d 是 dst 像素点
s 的每个分量乘以 d 的 alpha 分量之后除以 255
XFERMODE(SrcIn) { return s.approxMulDiv255(d.alphas() ); }
- 如果以硬件加速渲染,假如以 OpenGL ES 接口实现,则
https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glBlendFunc.xml
// 设置混合参数
void glBlendFunc(GLenum sfactor,
GLenum dfactor);
练习题
- 实践以上练习
总结
以上就是 Xfermode 的使用方法和计算原理,以后遇到各种遮罩、混合可以尝试使用它来解决。