Android图形处理--PorterDuff.Mode那些事儿

我们在绘制图形图像的时候经常会用到 PorterDuff.Mode,它对我们绘制图形有很大的帮助,如果我们对它不甚了解甚至根本不理解,那会是很麻烦的事情,我这篇博客就是来给大家介绍一下 PorterDuff.Mode。

1、基本介绍

在介绍 PorterDuff.Mode 之前,我们首先要了解一下 Xfermode。Xfermode 被许多人称为过渡模式,就是指图像的饱和度、颜色值等参数的计算结果的图像表现。Xfermode 是用来做图形渲染的,可以通过修改 Paint 的 Xfermode 来影响在 Canvas 已有的图像上面绘制新的颜色的方式 。

Xfermode 包含三个子类:AvoidXfermode、PixelXorXfermode 和 PorterDuffXfermode,前两个已经被弃用了,我们就不管它们了。

PorterDuffXfermode 是一个非常强大的转换模式,使用它,可以使用图像合成的 18条 Porter-Duff 规则的任意一条来控制 Paint 与已有的 Canvas 图像进行交互的方式。

看到这个名字大家都应该知道了,这肯定和我们要讲的 PorterDuff.Mode 有关,有什么关系就要说到 PorterDuff.Mode 的本质是什么,在 PorterDuff 里有个枚举变量 Mode,它有18个值,分别对应了一条图形混合的模式。

我们通过在 PorterDuffXfermode 里设置 PorterDuff.Mode,再把 PorterDuffXfermode 传给 Paint,就能实现很多我们想要的图形效果。

2、各种模式效果

PorterDuff.Mode 的效果相当惊人,但只有充分了解每种模式的作用,我们才能百战不怠。

public enum Mode {
    /** [0, 0] */
    CLEAR       (0),
    /** [Sa, Sc] */
    SRC         (1),
    /** [Da, Dc] */
    DST         (2),
    /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
    SRC_OVER    (3),
    /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
    DST_OVER    (4),
    /** [Sa * Da, Sc * Da] */
    SRC_IN      (5),
    /** [Sa * Da, Sa * Dc] */
    DST_IN      (6),
    /** [Sa * (1 - Da), Sc * (1 - Da)] */
    SRC_OUT     (7),
    /** [Da * (1 - Sa), Dc * (1 - Sa)] */
    DST_OUT     (8),
    /** [Da, Sc * Da + (1 - Sa) * Dc] */
    SRC_ATOP    (9),
    /** [Sa, Sa * Dc + Sc * (1 - Da)] */
    DST_ATOP    (10),
    /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
    XOR         (11),
    /** [Sa + Da - Sa*Da,
         Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
    DARKEN      (16),
    /** [Sa + Da - Sa*Da,
         Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
    LIGHTEN     (17),
    /** [Sa * Da, Sc * Dc] */
    MULTIPLY    (13),
    /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
    SCREEN      (14),
    /** Saturate(S + D) */
    ADD         (12),
    OVERLAY     (15);

    Mode(int nativeInt) {
        this.nativeInt = nativeInt;
}

    /**
     * @hide
     */
    public final int nativeInt;
}

这就是 PorterDuff.Mode 的定义,注释的文字是说明该模式的 alpha 通道和颜色值的计算方式,要理解各个模式的计算方式需要先弄明白公式中各个元素的具体含义:

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

当 Alpha 通道的值为1时,图像完全可见;当 Alpha 通道值为0时,图像完全不可见;当 Alpha 通道的值介于0和1之间时,图像只有一部分可见。Alpha 通道描述的是图像的形状,而不是透明度。

其实每一个值都是按一定的规则对像素点的颜色值进行加减乘除操作。但,千万不要用数学计算去套这些模式的操作结果,因为数学计算的结果和显示的颜色值是有一定的差别的。但,可以按数学计算公式去估算每一个模式中颜色值的大致变化。

其中的每一个值都是对src与dst的每一个像素点的颜色值进行操作。src指前景、上层,后绘制上的;dst指后景、下层,先绘制上去的。

以 SRC_OVER 的计算方式为例:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 。它以逗号分成两部分,前面“Sa + (1 - Sa)*Da”计算的值代表 SRC_OVER 模式的 Alpha 通道,后面“Rc = Sc + (1 - Sa)*Dc”计算的是 SRC_OVER模式的颜色值,图形混合后的图片依靠这个矢量来计算 ARGB 的值。

1、Bitmap绘制

先来上几张图片,图文并茂我认为是最好的让人理解的方法。

Android图形处理--PorterDuff.Mode那些事儿_第1张图片

Android图形处理--PorterDuff.Mode那些事儿_第2张图片

两个图形一圆一方通过一定的计算产生了不同的合成效果,我们在实际工作中需要做图片处理时可以参考这张图的示意快速地选择合适的 Mode。

最常用的就是第一幅图的16种,ADD 和 OVERLAY 并不常使用。

在源图像与目标图像不相交的部分,源图像可以把 Da、Dc 视作0,目标图也是如此。

1、CLEAR

清除模式,[0, 0],即图像中所有像素点的 alpha 和颜色值均为0,用这个就只有我们设置的背景。

2、SRC

[Sa, Sc],只保留源图像的 alpha 和 color ,所以绘制出来只有源图,上图中就是蓝色正方形。

3、DST

[Da, Dc],只保留了目标图像的 alpha 和 color,所以绘制出来的只有目标图,上图中就是圆形。

4、SRC_OVER

[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc],看上去是在目标图像上层绘制源图像,如果源图与目标图的重合部分源图不透明,则完全覆盖目标图;如果有透明的部分则会计算它们的颜色,我们说过算得就是每个像素的颜色值,所以该像素点是否有值还要看 color。

5、DST_OVER

[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc],与 SRC_OVER 相反,此模式是目标图像被绘制在源图像的上方,公式可以看到如果目标图不透明则这个像素的颜色就是目标图像的 color;透明则计算两图的颜色。

6、SRC_IN

[Sa * Da, Sc * Da],在两者相交的地方绘制源图像,并且绘制的效果会受到目标图像对应地方透明度的影响。这个公式更好理解。

7、DST_IN

[Sa * Da, Sa * Dc],理论和 SRC_IN 相同,在两者相交的地方绘制目标图像,并且绘制的效果会受到源图像对应地方透明度的影响。

8、SRC_OUT

[Sa * (1 - Da), Sc * (1 - Da)],从名字可以看出于 SRC_IN 相反,那可以理解为在不相交的地方绘制源图像。color 是 Sc * ( 1 - Da ) ,表示如果相交处的目标色的 alpha 是完全不透明的,这时候源图像会完全被过滤掉,否则会受到相交处目标色 alpha 影响,呈现出对应色值。

9、DST_OUT

[Da * (1 - Sa), Dc * (1 - Sa)],可以类比SRC_OUT , 在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处完全过滤,完全透明则不过滤,半透明则受到相交处源图像 alpha 影响,呈现出对应色值。

10、SRC_ATOP

[Da, Sc * Da + (1 - Sa) * Dc],从公式看,源图像没有和目标图像相交的部分因为 Da 和 Dc 没有值,视作0,所以那个部分就不会被绘制。因此这个模式就是在源图像和目标图像相交处绘制源图像,不相交的地方只绘制目标图像的部分,并且相交处的效果会受到源图像和目标图像alpha的影响。

11、DST_ATOP

[Sa, Sa * Dc + Sc * (1 - Da)],源图像和目标图像相交处绘制目标图像,不相交的地方绘制源图像的部分,并且相交处的效果会受到源图像和目标图像alpha的影响。

12、XOR

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

13、DARKEN

[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)],min(Sc,Dc) 表示选择 Sc 和 Dc 中更接近 0 的值,也就是更暗的颜色。所以该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合。

从公式上看,色值上如果都不透明则取较暗值,非完全不透明情况下使用上面算法进行计算,受到源图和目标图对应色值和 alpha 值影响。

14、LIGHTEN

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

15、MULTIPLY

[Sa * Da, Sc * Dc],类似于PS中的“正片叠底”效果,即查看每个通道中的颜色信息,并将基色与混合色复合。结果色总是较暗的颜色,这是二进制的与运算,黑色是0,白色是1,任何颜色与黑色复合产生黑色,任何颜色与白色复合保持不变,当用黑色或白色以外的颜色绘画时,绘画工具绘制的连续描边产生逐渐变暗的颜色。

16、SCREEN

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

17、ADD

Saturate(S + D),饱和度叠加

18、OVERLAY

像素是进行 Multiply(正片叠底)混合还是 Screen(屏幕)混合,取决于底层颜色,但底层颜色的高光与阴影部分的亮度细节会被保留。

推荐简书的一篇文章,各个击破搞明白PorterDuff.Mode,这里面的经过混合模式处理后的图片效果比官方提供给我们的上面的图要好的多,理解起来要简单的多,对模式的说明也让我想通了很多,我这里的说明就是在他的基础上再加上我的解释,让其详细了一些,大家可以看看。

2、Canvas直接绘制

这里要注意一下,我们上面的说明是围绕于目标图和源图都是 Bitmap 的情况下,如果是 Canvas 直接绘制的话,模式的功能就是下面这幅图:

Android图形处理--PorterDuff.Mode那些事儿_第3张图片

大致的意思是与上面相同的,在下面的例子也会出现直接用 Canvas 绘制的,大家来对照这幅图就能理解啦。

3、代码演示

我们学习 PorterDuff.Mode 就是为了将它运用到我们实际的开发当中,所以我们当然要知道在哪些地方能用到 PorterDuff.Mode,我总结了一些它的用法:

1、自定义loading样式

我相信大家在下载的时候很多时候都能看到下面的效果:

Android图形处理--PorterDuff.Mode那些事儿_第4张图片

我们把这个当做下载文件的进度,当蓝色填满了整个图形,那就代表下载完成了,是不是觉得效果很不错,要实现这个效果很简单,我们只要用到 PorterDuff 的一种模式就能轻而易举的达到,我们来看代码:

public class LogoLoadingView extends View {

    private int width, height;
    private Paint paint;
    private Bitmap bitmap;
    private int currentTop;
    private RectF rectF;
    private PorterDuffXfermode porterDuffXfermode;

    public LogoLoadingView(Context context) {
        this(context, null);
    }

    public LogoLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init() {

        paint = new Paint();
        //设置抗锯齿
        paint.setAntiAlias(true);
        //设置填充样式
        paint.setStyle(Paint.Style.FILL);
        //设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
        paint.setDither(true);
        //加快显示速度,本设置项依赖于dither和xfermode的设置
        paint.setFilterBitmap(true);
        //从资源文件中解析获取Bitmap
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);

        width = 200;
        height = 200;

        bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);

        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

        /**
         * 设置当前矩形的高度为0
         */
        currentTop = bitmap.getHeight();
        rectF = new RectF(0, currentTop, bitmap.getWidth(), bitmap.getHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        rectF.top = currentTop;

        /*
         * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)
         */
        int sc = canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG);

        //绘制目标图像(设置的安卓图标)
        canvas.drawBitmap(bitmap, 0, 0, null);

        paint.setXfermode(porterDuffXfermode);
        paint.setColor(Color.BLUE);

        //绘制
        canvas.drawRect(rectF, paint);
        paint.setXfermode(null);
        /**
         * 还原画布,与canvas.saveLayer配套使用
         */
        canvas.restoreToCount(sc);
        if (currentTop > 0) {
            currentTop -= 2;
            postInvalidate();
        }
    }
}

大部分的工作都是用来初始化我们的画笔,这些个方法的效果就不详细说了,我们得到图片后,因为原本的图片比较小,所以我用 createScaledBitmap() 调整为我想要的大小。我们在这里要用到的是 SRC_IN 模式,上面说过它的效果是在目标图与源图相交的部分,不透明就以源图的颜色为准。因为我们要在图形上填充颜色,所以目标图应该是 Android 机器人。

在这里使用 PorterDuff.Mode 是 SRC_IN,因为这里我们是直接在 Canvas 上绘制矩形,所以对照的是 Canvas 的那幅图,这里 SRC_IN 的效果和 SRC_ATOP 一样。

我们要做的是让颜色逐渐填满图形,所以最开始矩形的高度应该是0。因为我们屏幕的坐标系中,Y 轴的坐标向下越来越大,所以我们设置矩形 top 为图形的 height,其实是把位置定在了图形的最下方。top 和 bottom 值相同,矩形高度就为0了。

在 onDraw() 方法中绘制,先绘制的是目标图,然后设置好 PorterDuff.Mode,再绘制矩形。要让颜色从图形底部填充,要改的就是矩形 top 的值,往上就 Y 轴坐标减小,所以我们让 top 逐渐减小,再让它重新绘制即可。

2、整合图片

有时候我们会想把两张图片融合在一起,得到看起来很炫酷的样子,这样的效果也是可以用 PorterDuff.Mode 做出来的:

我这里下载了两张图片:

Android图形处理--PorterDuff.Mode那些事儿_第5张图片

Android图形处理--PorterDuff.Mode那些事儿_第6张图片

我们要做的就是让莲花出现在背景图上。

public class SrcTopView extends View {

    private Paint mPaint;// 画笔
    private Bitmap bitmapDis, bitmapSrc;// 位图
    private PorterDuffXfermode porterDuffXfermode;// 图形混合模式

    private int x, y;// 位图绘制时左上角的起点坐标
    private int screenW, screenH;// 屏幕尺寸

    public SrcTopView(Context context) {
        this(context, null);
    }

    public SrcTopView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 实例化混合模式
        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);

        // 初始化画笔
        initPaint();

        // 初始化资源
        initRes(context);
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {

        mPaint = new Paint();

        mPaint.setAntiAlias(true);

        mPaint.setStyle(Paint.Style.FILL);

        mPaint.setDither(true);

        mPaint.setFilterBitmap(true);
    }

    /**
     * 初始化资源
     */
    private void initRes(Context context) {
        // 获取位图
        bitmapDis = BitmapFactory.decodeResource(context.getResources(), R.drawable.picture1);
        bitmapSrc = BitmapFactory.decodeResource(context.getResources(), R.drawable.picture2);

        int[] screenSize = MeasureUtil.getScreenSize(context);

        // 获取屏幕尺寸
        screenW = screenSize[0];
        screenH = screenSize[1];

        /*
         * 计算位图绘制时左上角的坐标使其位于屏幕中心
         * 屏幕坐标x轴向左偏移位图一半的宽度
         * 屏幕坐标y轴向上偏移位图一半的高度
         */
        x = screenW / 2;
        y = screenH / 2;
    }

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

        int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);

        // 先绘制dis目标图
        canvas.drawBitmap(bitmapDis, x - bitmapDis.getWidth() / 2,
                y - bitmapDis.getHeight() / 2, mPaint);

        // 设置混合模式
        mPaint.setXfermode(porterDuffXfermode);

        // 再绘制src源图
        canvas.drawBitmap(bitmapSrc, x - bitmapSrc.getHeight() / 2,
                y - bitmapSrc.getWidth() / 2, mPaint);

        // 还原混合模式
        mPaint.setXfermode(null);

        // 还原画布
        canvas.restoreToCount(sc);
    }
}
public class MeasureUtil {

    public static int[] getScreenSize(Context context) {
        DisplayMetrics metrics = new DisplayMetrics();
        metrics = context.getResources().getDisplayMetrics();

        int[] sizes = new int[] {
                metrics.widthPixels, metrics.heightPixels
        };

        return sizes;
    }
}

我们用 MeasureUtil 获得屏幕的宽高,在绘制 Bitmap 的时候设置好 left 和 top 的坐标,要注意的就是这个了,实现是很简单的。

Android图形处理--PorterDuff.Mode那些事儿_第7张图片

3、裁图得到圆形圆角

我们也可以使用 PorterDuff.Mode 来让图片变成圆形或者出现圆角:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    setLayerType(LAYER_TYPE_SOFTWARE, null); //关闭硬件加速

    Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);

    int radiu = Math.min(src.getHeight(), src.getWidth()) / 2;
    canvas.drawCircle(src.getWidth() / 2, src.getHeight() / 2, radiu, mPaint);

    //设置Xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    //源图
    canvas.drawBitmap(src, 0, 0, mPaint);

    //还原Xfermode
    mPaint.setXfermode(null);
}

Android图形处理--PorterDuff.Mode那些事儿_第8张图片

得到圆角也就是改变一个方法的事情:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    setLayerType(LAYER_TYPE_SOFTWARE, null); //关闭硬件加速

    Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.picture1);

    //得到目标图
    RectF rectF = new RectF(0, 0, src.getWidth(), src.getHeight());
    canvas.drawRoundRect(rectF, 70, 70, mPaint);

    //设置Xfermode
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    //源图
    canvas.drawBitmap(src, 0, 0, mPaint);

    //还原Xfermode
    mPaint.setXfermode(null);
}

Android图形处理--PorterDuff.Mode那些事儿_第9张图片

关于 PorterDuff.Mode 的介绍就讲到这里,在刚了解它的时候,我们可能不能第一时间想到使用,这就需要我们大家的努力练习啦。

结束语:本文仅用来学习记录,参考查阅。

你可能感兴趣的:(UI绘制,android)