Android图像合成模式之PorterDuff.Mode

本文转载自简书《各个击破搞明白PorterDuff.Mode》,链接地址如下:https://www.jianshu.com/p/d11892bbe055

PorterDuff.Mode这个枚举变量主要用于Android中的图像合成

PorterDuff.Mode是什么

在android SDK Paint类中有一个很重要的方法setXfermode(源码如下)

public Xfermode setXfermode(Xfermode xfermode) {
    long xfermodeNative = 0;
    if (xfermode != null)
        xfermodeNative = xfermode.native_instance;
    native_setXfermode(mNativePaint, xfermodeNative);
    mXfermode = xfermode;
    return xfermode;
}

这个方法用于设置图像的过渡模式,所谓过渡是指图像的饱和度、颜色值等参数的计算结果的图像表现。在SDK中Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,前两个类在API 16被遗弃了,而且不是本文的主题内容,所以这里不作介绍。PorterDuffXfermode类主要用于图形合成时的图像过渡模式计算,其概念来自于1984年在ACM SIGGRAPH计算机图形学出版物上发表了“Compositing digital images(合成数字图像)”的Tomas Porter和Tom Duff,合成图像的概念极大地推动了图形图像学的发展,PorterDuffXfermode类名就来源于这俩人的名字组合PorterDuff。下面是android SDK中PorterDuff的Mode枚举类型定义。图片出自这里
这里写图片描述

上面图中每种模式的注释都说明了该模式的alpha通道和颜色值的计算方式,要理解各个模式的计算方式需要先弄明白公式中各个元素的具体含义:

Sa:全称为Source alpha,表示源图的Alpha通道;
Sc:全称为Source color,表示源图的颜色;
Da:全称为Destination alpha,表示目标图的Alpha通道;
Dc:全称为Destination color,表示目标图的颜色.

当Alpha通道的值为1时,图像完全可见;当Alpha通道值为0时,图像完全不可见;当Alpha通道的值介于0和1之间时,图像只有一部分可见。Alpha通道描述的是图像的形状,而不是透明度。
以SCREEN的计算方式为例:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],“[……]”里分为两部分,其中“,”前的部分“Sa + Da - Sa * Da”计算的值代表SCREEN模式的Alpha通道,而“,”后的部分“Sc + Dc - Sc * Dc”计算SCREEN模式的颜色值,图形混合后的图片依靠这个矢量来计算ARGB的值。

如何应用PorterDuff.Mode

图像合成效果示意图
上面这张图从一定程度上形象地说明了运用PorterDuff.Mode进行图像合成的作用,两个图形一圆一方通过一定的计算产生了不同的合成效果,我们在实际工作中需要做图片处理时可以参考这张图的示意快速地选择合适的Mode。
为了更清楚地理解各个Mode的作用效果,我自己写了一个Demo,逐一验证上面的Mode。

public class PorterDuffXfermodeView extends View {
    private Paint mPaint;
    private Bitmap dstBmp, srcBmp;
    private RectF dstRect, srcRect;

    private Xfermode mXfermode;
    private PorterDuff.Mode mPorterDuffMode = PorterDuff.Mode.CLEAR;


    public PorterDuffXfermodeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        dstBmp = BitmapFactory.decodeResource(getResources(), R.mipmap.destination);
        srcBmp = BitmapFactory.decodeResource(getResources(), R.mipmap.source);
        mXfermode = new PorterDuffXfermode(mPorterDuffMode);

    }

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

        //背景色设为白色,方便比较效果
        canvas.drawColor(Color.WHITE);
        //将绘制操作保存到新的图层,将用到硬件加速,这里将图像合成的处理放到离屏缓存中进行
        int saveCount = canvas.saveLayer(srcRect, mPaint, Canvas.ALL_SAVE_FLAG);
        //先绘制的是目标图
        canvas.drawBitmap(dstBmp, null, dstRect, mPaint);
        //设置混合模式
        mPaint.setXfermode(mXfermode);
        //后绘制的是源图
        canvas.drawBitmap(srcBmp, null, srcRect, mPaint);
        //清除混合模式
        mPaint.setXfermode(null);
        //还原画布
        canvas.restoreToCount(saveCount);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int width = w <= h ? w : h;
        int centerX = w / 2;
        int centerY = h / 2;
        int quarterWidth = width / 4;
        srcRect = new RectF(centerX - quarterWidth, centerY - quarterWidth, centerX + quarterWidth, centerY + quarterWidth);
        dstRect = new RectF(centerX - quarterWidth, centerY - quarterWidth, centerX + quarterWidth, centerY + quarterWidth);
    }

    public void setXfermode(PorterDuff.Mode mode) {
        this.mPorterDuffMode = mode;
        mXfermode = new PorterDuffXfermode(mPorterDuffMode);
        invalidate();
    }

}

使用测试Acitivity.java

public class PorterDuffActivity extends Activity {
    private Spinner spinner;
    private PorterDuffXfermodeView xfermodeview;
    private List<String> modeLists;
    private TextView tv;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_porter_duff);
        initView();
    }

    private void initView() {
        modeLists = new ArrayList<>();
        spinner = (Spinner) findViewById(R.id.spinner);
        xfermodeview = (PorterDuffXfermodeView) findViewById(R.id.porterView);
        tv = (TextView) findViewById(R.id.tv);
        modeLists.add("CLEAR");
        modeLists.add("SRC");
        modeLists.add("DST");
        modeLists.add("SRC_OVER");
        modeLists.add("DST_OVER");
        modeLists.add("SRC_IN");
        modeLists.add("DST_IN");
        modeLists.add("SRC_OUT");
        modeLists.add("DST_OUT");
        modeLists.add("SRC_ATOP");
        modeLists.add("DST_ATOP");
        modeLists.add("XOR");
        modeLists.add("DARKEN");
        modeLists.add("LIGHTEN");
        modeLists.add("MULTIPLY");
        modeLists.add("SCREEN");
        spinner.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, modeLists));
        spinner.setSelection(0);
      //  xfermodeview.setXfermode(PorterDuff.Mode.CLEAR);
        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                Log.e("选择---->", modeLists.get(i));
                setxfermode(modeLists.get(i));
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });
    }

    private void setxfermode(String s) {
        switch (s) {
            case "CLEAR":
                xfermodeview.setXfermode(PorterDuff.Mode.CLEAR);
                tv.setText("清除模式,[0, 0],即相交图像中所有像素点的alpha和颜色值均为0");
                break;
            case "SRC":
                xfermodeview.setXfermode(PorterDuff.Mode.SRC);
                tv.setText("[Sa, Sc],只保留源图像的 alpha 和 color ,所以绘制出来只有源图,如source");
                break;
            case "DST":
                xfermodeview.setXfermode(PorterDuff.Mode.DST);
                tv.setText("[Da, Dc],只保留了目标图像的alpha和color值,所以绘制出来的只有目标图,如destination。");
                break;
            case "SRC_OVER":
                xfermodeview.setXfermode(PorterDuff.Mode.SRC_OVER);
                tv.setText("[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc],在目标图像上层绘制源图像");
                break;
            case "DST_OVER":
                xfermodeview.setXfermode(PorterDuff.Mode.DST_OVER);
                tv.setText("[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],与SRC_OVER相反,此模式是目标图像被绘制在源图像的上方");
                break;
            case "SRC_IN":
                xfermodeview.setXfermode(PorterDuff.Mode.SRC_IN);
                tv.setText("[Sa * Da, Sc * Da],在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响");
                break;
            case "DST_IN":
                xfermodeview.setXfermode(PorterDuff.Mode.DST_IN);
                tv.setText("[Sa * Da, Sa * Dc],可以和SRC_IN 进行类比,在两者相交的地方绘制目标图像,并且绘制的效果会受到源图像对应地方透明度的影响");
                break;
            case "SRC_OUT":
                tv.setText("[Sa * (1 - Da), Sc * (1 - Da)],从字面上可以理解为在不相交的地方绘制源图像\ncolor 是 Sc * ( 1 - Da ) ," +
                        "表示如果相交处的目标色的alpha是完全不透明的,这时候源图像会完全被过滤掉,否则会受到相交处目标色 alpha 影响,呈现出对应色值。");
                xfermodeview.setXfermode(PorterDuff.Mode.SRC_OUT);
                break;
            case "DST_OUT":
                tv.setText("[Da * (1 - Sa), Dc * (1 - Sa)],可以类比SRC_OUT , 在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处则完全过滤,完全透明则不过滤");
                xfermodeview.setXfermode(PorterDuff.Mode.DST_OUT);
                break;
            case "SRC_ATOP":
                tv.setText("[Da, Sc * Da + (1 - Sa) * Dc],源图像和目标图像相交处绘制源图像,不相交的地方绘制目标图像,并且相交处的效果会受到源图像和目标图像alpha的影响");
                xfermodeview.setXfermode(PorterDuff.Mode.SRC_ATOP);
                break;
            case "DST_ATOP":
                tv.setText("[Sa, Sa * Dc + Sc * (1 - Da)],源图像和目标图像相交处绘制目标图像,不相交的地方绘制源图像,并且相交处的效果会受到源图像和目标图像alpha的影响");
                xfermodeview.setXfermode(PorterDuff.Mode.DST_ATOP);
                break;
            case "XOR":
                tv.setText("[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc],在不相交的地方按原样绘制源图像和目标图像,相交的地方受到对应alpha和颜色值影响,按公式进行计算,如果都完全不透明则相交处完全不绘制");
                xfermodeview.setXfermode(PorterDuff.Mode.XOR);
                break;
            case "DARKEN":
                tv.setText("[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)],该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合;\n" +
                        "从算法上看,alpha值变大,色值上如果都不透明则取较暗值,非完全不透明情况下使用上面算法进行计算,受到源图和目标图对应色值和alpha值影响。 ");
                xfermodeview.setXfermode(PorterDuff.Mode.DARKEN);
                break;
            case "LIGHTEN":
                tv.setText("[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],可以和 DARKEN 对比起来看,DARKEN 的目的是变暗,LIGHTEN 的目的则是变亮,如果在均完全不透明的情况下,色值取源色值和目标色值中的较大值,否则按上面算法进行计算。");
                xfermodeview.setXfermode(PorterDuff.Mode.LIGHTEN);
                break;
            case "MULTIPLY":
                tv.setText("[Sa * Da, Sc * Dc],正片叠底,即查看每个通道中的颜色信息,并将基色与混合色复合。结果色总是较暗的颜色,任何颜色与黑色复合产生黑色,任何颜色与白色复合保持不变,当用黑色或白色以外的颜色绘画时,绘画工具绘制的连续描边产生逐渐变暗的颜色。");
                xfermodeview.setXfermode(PorterDuff.Mode.MULTIPLY);
                break;
            case "SCREEN":
                tv.setText("[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],滤色,滤色模式与我们所用的显示屏原理相同,所以也有版本把它翻译成屏幕;简单的说就是保留两个图层中较白的部分,较暗的部分被遮盖;当一层使用了滤色(屏幕)模式时,图层中纯黑的部分变成完全透明,纯白部分完全不透明,其他的颜色根据颜色级别产生半透明的效果。");
                xfermodeview.setXfermode(PorterDuff.Mode.SCREEN);
                break;
        }

    }

布局文件xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/destination" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/source" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="目标图像" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="源图像" />
    </LinearLayout>

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#990000"
        android:textSize="18sp" />

    <com.lancoo.myapplication.PorterDuffXfermodeView
        android:id="@+id/porterView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

整体效果图如下
这里写图片描述
这里写图片描述
这里用到了2张图片,destination沿图片左上到右下的对角线将图片分成两部分,左下部分是完全不透明的,右上部分是半透明的;source沿图片左下到右上的对角线将图片分成两部分,左上部分是半透明的,右下部分是完全不透明的。图中有颜色的区域外都是完全透明的,这样当两张图片合成时可以让各种情况交叉呈现,便于分析理解。
destination
destination 目标图

source
source 源图

CLEAR
清除模式,[0, 0],即图像中所有像素点的alpha和颜色值均为0,Demo中的实际效果就是白色背景。注:这里有个坑,对于sdkversion大于等于11时,需要关闭硬件加速,否则 Mode.CLEAR 、 Mode.DARKEN 、 Mode.LIGHTEN 三种模式下绘制效果不正常。怎么关闭?可以在当前Activity内的添加 android:hardwareAccelerated="false"

SRC
[Sa, Sc],只保留源图像的 alpha 和 color ,所以绘制出来只有源图,如source。有时候会感觉分不清先绘制的是源图还是后绘制的是源图,这个时候可以这么记,先绘制的是目标图。

DST
[Da, Dc],只保留了目标图像的alpha和color值,所以绘制出来的只有目标图,如destination。

SRC_OVER
[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc],在目标图像上层绘制源图像
这里写图片描述


DST_OVER
[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],与SRC_OVER相反,此模式是目标图像被绘制在源图像的上方
这里写图片描述

SRC_IN
[Sa * Da, Sc * Da],在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响
这里写图片描述

DST_IN
[Sa * Da, Sa * Dc],可以和SRC_IN 进行类比,在两者相交的地方绘制目标图像,并且绘制的效果会受到源图像对应地方透明度的影响
这里写图片描述

SRC_OUT
[Sa * (1 - Da), Sc * (1 - Da)],从字面上可以理解为在不相交的地方绘制源图像,那么我们来看看效果是不是这样,如下图。实际上color 是 Sc * ( 1 - Da ) ,表示如果相交处的目标色的alpha是完全不透明的,这时候源图像会完全被过滤掉,否则会受到相交处目标色 alpha 影响,呈现出对应色值。
这里写图片描述

DST_OUT
[Da * (1 - Sa), Dc * (1 - Sa)],可以类比SRC_OUT , 在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处则完全过滤,完全透明则不过滤
这里写图片描述

SRC_ATOP
[Da, Sc * Da + (1 - Sa) * Dc],源图像和目标图像相交处绘制源图像,不相交的地方绘制目标图像,并且相交处的效果会受到源图像和目标图像alpha的影响
这里写图片描述

DST_ATOP
[Sa, Sa * Dc + Sc * (1 - Da)],源图像和目标图像相交处绘制目标图像,不相交的地方绘制源图像,并且相交处的效果会受到源图像和目标图像alpha的影响
这里写图片描述

XOR
[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc],在不相交的地方按原样绘制源图像和目标图像,相交的地方受到对应alpha和颜色值影响,按公式进行计算,如果都完全不透明则相交处完全不绘制
这里写图片描述

DARKEN
[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)],该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合;
从算法上看,alpha值变大,色值上如果都不透明则取较暗值,非完全不透明情况下使用上面算法进行计算,受到源图和目标图对应色值和alpha值影响。
这里写图片描述

LIGHTEN
[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],可以和 DARKEN 对比起来看,DARKEN 的目的是变暗,LIGHTEN 的目的则是变亮,如果在均完全不透明的情况下,色值取源色值和目标色值中的较大值,否则按上面算法进行计算。
这里写图片描述

MULTIPLY
[Sa * Da, Sc * Dc],正片叠底,即查看每个通道中的颜色信息,并将基色与混合色复合。结果色总是较暗的颜色,任何颜色与黑色复合产生黑色,任何颜色与白色复合保持不变,当用黑色或白色以外的颜色绘画时,绘画工具绘制的连续描边产生逐渐变暗的颜色。
这里写图片描述

SCREEN
[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc],滤色,滤色模式与我们所用的显示屏原理相同,所以也有版本把它翻译成屏幕;简单的说就是保留两个图层中较白的部分,较暗的部分被遮盖;当一层使用了滤色(屏幕)模式时,图层中纯黑的部分变成完全透明,纯白部分完全不透明,其他的颜色根据颜色级别产生半透明的效果。
这里写图片描述

参考文献:
Android PorterDuffXfermode使用中的一些坑
自定义控件其实很简单1/6

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值