Android自定义View系列:PorterDuff.Mode

1 PorterDuff.Mode的作用和背景

PorterDuff.Mode 是用来指定两个图像共同绘制时的颜色策略的。它是一个枚举,不同的Mode可以指定不同的策略。颜色策略的意思,就是说把源图像绘制到目标图像处时应该怎样确定二者结合后的颜色。

PorterDuff.Mode 一共有17个,可以分为两类:

  • Alpha合成(Alpha Compositing)

  • 混合(Blending)

第一类Alpha合成,其实就是 PorterDuff 这个词所指代的算法。PorterDuff 并不是一个具有实际意义的词组,而是两个人的名字。这两个人当年共同发表了一篇论文,描述了12种将两个图像共同绘制的操作(即算法)。而这篇论文所论述的操作,都是关于Alpha通道(也就是我们通俗理解的透明度)的计算的,后来人们就把这类计算称为Alpha合成(Alpha Composiing)。

第二类混合,也就是Photoshop等制图软件里都有的那些混合模式(multiply darken lighten之类的)。这一类操作的是颜色本身而不是Alpha通道,并不属于Alpha合成,所以和Porter与Duff这两个人也没什么关系,不过为了使用方便,它们同样也被Google加进了 PorterDuff.Mode 里。

2 PorterDuff.Mode的颜色策略

2.1 Alpha合成

在这里插入图片描述
在这里插入图片描述

上面Google的官方文档展示的Alpha合成是DST在下层先绘制,SRC在上层后绘制

DST 就是 Destination 意思是目标,在使用Xfermode前我们应该要有一个目标,就是 DST;而 SRC 就是 Source 意思是源,就是我要往 DST 上贴使用Xfermode的图像。所以就有DST在下层,SRC在上层。

根据上面的Alpha合成效果,我这边再细分方便理解:

  • OUT:取指定层的非交集部分。比如 DST_OUT 表示取下层非交集部分

  • IN:取指定层的交集部分。比如 DST_IN 表示取下层交集部分

  • OVER:指定层在上层显示。比如 DST_OVER 表示取下层显示在上面

  • ATOP:取与指定层的交集部分和相反层的非交集部分。比如 DST_ATOP 表示取下层的交集部分和上层的非交集部分

  • XOR:取上层和下层的非交集部分

那么各个Alpha合成的描述如下:

  • PorterDuff.Mode.CLEAR:绘制不会提交到画布上

  • PorterDuff.Mode.SRC:显示上层绘制图片

  • PorterDuff.Mode.SRC_OVER:正常显示,指定上层覆盖在下层上面

  • PorterDuff.Mode.SRC_IN:取上层交集部分

  • PorterDuff.Mode.SRC_OUT:取上层非交集部分

  • PorterDuff.Mode.SRC_ATOP:取上层交集部分和下层非交集部分

  • PorterDuff.Mode.DST:显示下层绘制图片

  • PorterDuff.Mode.DST_OVER:下层覆盖在上层上面

  • PorterDuff.Mode.DST_IN:取下层交集部分

  • PorterDuff.Mode.DST_OUT:取下层非交集部分

  • PorterDuff.Mode.DST_ATOP:取下层交集部分和上层非交集部分

  • PorterDuff.Mode.CLEAR:所绘制不会提交到画布上

  • PorterDuff.Mode.XOR:取上层和下层的非交集部分

2.2 Xfermode使用注意事项

2.2.1 控制好透明区域

你或许会认为Google官方文档中,SRC和DST就是只绘制颜色区域一个矩形和一个圆形然后两个颜色区域叠加,你的理解是这样的:

SRC:

// 假设矩形的绘制区域是在left=150, top=150, right=400, bottom=400
// 不包含外部透明区域
canvas.drawRect(150, 150, 400, 400, paint);

在这里插入图片描述

DST:

// 假设圆形的绘制区域是在圆心(400, 150),radius=150区域
// 不包含外部透明区域
canvas.drawCircle(400, 150, 150, paint);

在这里插入图片描述

// 只考虑颜色区域的叠加效果
public class XfermodeView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Xfermode mXfermode;

    public XfermodeView(Context context) {
        super(context);
    }

    public XfermodeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public XfermodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    {
        // 如果有效果没有生效(比如混合的几种模式),需要关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);

		// 绘制DST位于下层,DST为圆形
        canvas.drawCircle(400, 150, 150, paint);

        mPaint.setXfermode(mXfermode);

		// 绘制SRC位于上层,SRC为矩形
        canvas.drawRect(150, 150, 400, 400, paint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(saved);
    }
}

如果按上面只绘制颜色区域的叠加,你会发现实际绘制效果和Google效果图展示的不同,好像有些颜色策略显示正常,有些显示不正确。这其实是透明区域控制不正确导致

Google官方文档展示所绘制SRC和DST是Bitmap,Bitmap有透明区域和颜色区域,Bitmap绘制的蓝色矩形和粉色圆形是用 Canvas 绘制的颜色区域宽高。SRC和DST的Bitmap是大小控制了透明区域重叠的,不仅仅包含颜色区域,还有外部的透明区域也要计算进去。描述如下图所示:

在这里插入图片描述

透明区域要足够覆盖到要和它结合绘制的内容,否则得到的结果很可能不是你想要的:

在这里插入图片描述
由上图所示,由于透明区域过小而覆盖不到的地方,将不会收到Xfermode的影响。

为了能够实现官方文档上的效果,我们需要的是把要合成效果的图像抠出来排除被空白区域影响,那要怎么抠出来?其实就是使用离屏缓冲 canvas.saveLayer()canvas.restoreToCount()

我们按照Google官方文档的效果将上面的分析写成项目示例代码:

public class XfermodeView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Bitmap mSrcBitmap;
    private Bitmap mDstBitmap;
    private Xfermode mXfermode;

    public XfermodeView(Context context) {
        super(context);
    }

    public XfermodeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public XfermodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    {
        // 如果有效果没有生效(比如混合的几种模式),需要关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mSrcBitmap == null) {
            mSrcBitmap = createSrc(getWidth(), getHeight());
        }
        if (mDstBitmap == null) {
            mDstBitmap = createDst(getWidth(), getHeight());
        }

        int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);

		// 绘制DST位于下层,DST为圆形
        canvas.drawBitmap(mDstBitmap, 0, 0, mPaint);

        mPaint.setXfermode(mXfermode);

		// 绘制SRC位于上层,SRC为矩形
        canvas.drawBitmap(mSrcBitmap, 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(saved);
    }

    private Bitmap createSrc(int width, int height) {
    	// Bitmap大小是View的宽高,和DST是重叠的
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.parseColor("#3C92F5"));
        canvas.drawRect(150, 150, 400, 400, paint);
        return bitmap;
    }

    private Bitmap createDst(int width, int height) {
    	// Bitmap大小是View的宽高,和SRC是重叠的
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.parseColor("#ED1462"));
        canvas.drawCircle(400, 150, 150, paint);
        return bitmap;
    }
}

2.2.2 使用离屏缓冲(Off-screen Buffer)

如果在使用Xfermode时没有开启离屏缓冲,会出现绘制效果与期望不符。同样按照上面SRC和DST的Bitmap都是同样大小来绘制,没有开启离屏缓冲效果的 PorterDuff.Mode.DST_IN 的效果如下:

在这里插入图片描述

按照逻辑我们会认为,在第二步绘制SRC画圆的时候,跟它共同计算的是第一步绘制的DST矩形。但实际上,却是整个View的显示区域都在画圆的时候参与计算,并且View自身的底色并不是默认的透明色,导致不仅绘制的是整个圆的范围,而且在范围之外都变成了黑色。

要想使用Xfermode正常绘制,必须使用离屏缓冲(Off-screen Buffer)把内容绘制在额外的层上,再把绘制好的内容贴回View中。通过使用离屏缓冲,把要绘制的内容单独绘制在缓冲层,Xfermode的使用就不会出现奇怪的结果了。使用离屏缓冲有两种方式:

  • Canvas.saveLayer():可以做短时的离屏缓冲
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
...
canvas.restoreToCount(saved);
  • View.setLayerType():直接把整个View都绘制在离屏缓冲中

    • setLayerType(LAYER_TYPE_HARDWARE):使用GPU来缓冲

    • setLayerType(LAYER_TYPE_SOFTWARE):直接用一个Bitmap来缓冲

2.3 混合

混合的使用也可以使用上面的Demo来完成,但要注意关闭硬件加速否则存在无法生效的问题。一般实际开发用得不多了解即可,具体效果还是直接拿的Google官方文档的效果图:

在这里插入图片描述

参考:

Hencoder自定义View

Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解

### AndroidPorterDuff.Mode 的用法 `PorterDuff.Mode` 是 Android 提供的一个类,用于定义图像混合模式。它主要用于 `Canvas` 绘图操作以及视图背景颜色或图片的叠加效果处理。通过设置不同的 `PorterDuff.Mode` 值,可以实现多种视觉效果。 #### 主要用途 1. **Drawable 颜色过滤**: 使用 `setColorFilter()` 方法为 Drawable 设置颜色滤镜。 2. **Bitmap 合成**: 在 Canvas 上绘制多个 Bitmap 时应用合成模式。 3. **View 背景着色**: 修改 View 的背景颜色并应用特定的效果。 以下是常见的几种 `PorterDuff.Mode` 及其作用: | Mode | 描述 | |-----------------|----------------------------------------------------------------------| | CLEAR | 输出透明像素,忽略源和目标的颜色[^2] | | SRC | 完全显示源图像,忽略目标图像[^2] | | DST | 显示目标图像,忽略源图像[^2] | | SRC_OVER | 默认行为,源图像覆盖在目标图像之上[^2] | | DST_OVER | 目标图像覆盖在源图像之上[^2] | | SRC_IN | 源图像仅保留与目标图像重叠的部分[^2] | | DST_IN | 目标图像仅保留与源图像重叠的部分[^2] | #### 示例代码:Drawable 颜色过滤 下面是一个简单的例子,展示如何使用 `PorterDuff.Mode.SRC_ATOP` 来改变 ImageView 图标的颜色。 ```java ImageView imageView = findViewById(R.id.image_view); Drawable drawable = ContextCompat.getDrawable(this, R.drawable.icon); if (drawable != null) { drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP); // 将图标变为红色 imageView.setImageDrawable(drawable); } ``` #### 示例代码:Button 背景颜色修改 如果希望动态更改 Button 的背景颜色,并应用一种特殊的混合模式,可以这样做: ```java Button button = findViewById(R.id.button); Drawable background = button.getBackground(); if (background != null) { background.setColorFilter(Color.BLUE, PorterDuff.Mode.MULTIPLY); // 应用 Multiply 效果 button.setBackground(background); } ``` 以上代码会将按钮的原始背景颜色与蓝色相乘,从而创建一个新的渐变效果。 --- ### 注意事项 - 不同设备可能对某些复杂的 `PorterDuff.Mode` 支持程度不同,在实际开发中需注意兼容性测试。 - 如果需要更高级的功能(如自定义 Shader),建议结合 OpenGL 或其他图形库来完成复杂渲染任务。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值