第8章 CustomView混合模式

混合模式相关知识是Paint绘图中最难的部分,它能够将两张图片无缝结合,实现类似Photoshop中的两张图片融合效果。

混合模式是通过Paint类中的Xfermode setXfermode(Xfermode xfermode)函数实现的,它的参数Xfermode是一个空类,主要依靠它的子类来实现不同的功能。

Xfermode is the base class for objects that are called to implement custom "transfer-modes" in the drawing pipeline. The static function Create(Modes) can be called to return an instance of any of the predefined subclasses as specified in the Modes enum. When an Xfermode is assigned to an Paint, then objects drawn with that paint have the xfermode applied.

一、混合模式之PorterDuffXfermode

https://developer.android.google.cn/reference/android/graphics/PorterDuff.Mode

PorterDuffXfermode概述:

public class Xfermode
extends Object

java.lang.Object
   ↳android.graphics.Xfermode
Known direct subclasses

PorterDuffXfermode

public PorterDuffXfermode(PorterDuff.Mode mode)

 构造函数只有一个参数PorterDuff.Mode,表示混合模式,枚举值有18个,表示各种混合模式,每种模式都对应着一种算法。

比如,LIGHTEN的计算方式为:

[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
Sa:Source alpha,表示源图像的Alpha通道
Sc:Source color,表示源图像的颜色
Da:Destination alpha,表示目标图像的Alpha通道
Dc:Destination color,表示目标图像的颜色     

每个公式中,都会分为两部分[..., ...],其中“,” 前部分的值代表计算后的Alpha通道;后部分的值代表计算后的颜色值。
图形混合后的图片就是依据这个公式来对DST和SRC两张图片中的每个像素进行计算,得到最终结果的。 

上面公式涉及两个概念:目标图像(DST)和源图像(SRC)。

这里创建两张图片,一个圆形和一个矩形,矩形的左上角开始的位置在圆形中心。

     

以上图片中,圆形是目标图像,矩形是源图像。很明显,如右图所标注的区域1就是源图像与目标图像的相交区域,区域2是源图像与空白像素的相交区域。其中,区域1和区域2在后面几节中都将用到,需提前注意。

在使用Xfermode时,为保险起见,需要做两件事:

(1)禁用硬件加速

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

(2)使用离屏绘制

// 新建图层
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
// 核心代码
...
// 还原图层
canvas.restoreToCount(layerId);

有关离屏绘制的原因,这里就先不引申了,后面会有单独的章节讲述离屏绘制,大家只需知道,我们需要把绘制的核心代码放在canvas.save()和canvas.restore()函数之间即可。

自定义一个控件并且初始化。

public class PorterDuffXfermodeView extends View {
    private int width = 200;
    private int height = 200;
    private Bitmap dstBmp;
    private Bitmap srcBmp;
    private Paint mPaint;
    public PorterDuffXfermodeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        dstBmp = makeDst(width, height);
        srcBmp = makeSrc(width, height);
        mPaint = new Paint();
    }
    ...
}
private Bitmap makeDst(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
    p.setColor(0xFFFFCC44);// 黄色
    c.drawOval(new RectF(0, 0, w, h), p);
    return bm;
}

 这里新建了一张空白图片,然后在图片上画一个黄色的圆形。所以此时的图片是中间有一个圆形的位图,除圆形以外的位置都是空白像素。

private Bitmap makeSrc(int w, int h) {
    Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bm);
    Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
    p.setColor(0xFF66AAFF);// 蓝色
    c.drawRect(0, 0, w, h, p);
    return bm;
}

同样,这里新建了一张相同大小的位图,并且将其填充为蓝色。 

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
    canvas.drawBitmap(dstBmp, 0, 0, mPaint);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(srcBmp, width/2, height/2, mPaint);
    mPaint.setXfermode(null);
    canvas.restoreToCount(layerId);
}

在离屏绘制部分,先在(0,0)位置把圆形图像画出来,然后设置PorterDuffXfermode的模式为Mode.SRC_IN,之后再以圆心中心点为左上角点画出矩形,最后清空Xfermode。

在Xfermode设置前画出的图像叫作目标图像,即给谁应用Xfermode;在Xfermode设置之后画出的图像叫作源图像,即拿什么应用Xfermode。

对于Mode.SRC_IN,它的计算公式为[Sa*Da, Sc*Da]。在这个公式中,结果值的透明度和颜色值都是由Sa、Sc分别乘以目标图像的Da来计算的。当目标图像为空白像素时,计算结果也将会为空白像素;当目标图像不透明时,相交区域将显示源图像像素。所以,从效果图中可以看出,区域1的两图像相交部分显示的是源图像;而对于区域2的不相交部分,此时目标图像的透明度是0,源图像不显示。

总的来讲,SRC_IN模式是在相交时利用目标图像的透明度来改变源图像的透明度和饱和度的。当目标图像的透明度是0,源图像不显示。

上例中,我们要首重关注三个区域:区域1(两图像相交的部分)、区域2(源图像的非相交部分)、未标注区域的目标图像的非相交部分。

颜色叠加相关模式:

针对色彩变换的几种模式:

Mode.ADD:饱和度相加
Mode.LIGHTEN:变亮
Mode.DARKEN:变暗
Mode.MULTIPLY:正片叠底
Mode.OVERLAY:叠加
Mode.SCREEN:滤色

1.Mode.ADD(饱和度相加)

对应算法:Saturate(S + D),即对SRC和DST两张图片相交区域的饱和度进行相加。

所谓的饱和度,指的其实是色彩的纯度,纯度越高,表现越鲜明,纯度较低,表现则较黯淡。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。

从效果图可看出,只有源图像与目标图像相交部分的图像饱和度产生了变化,因为在不相交的地方,只有一方的饱和度是100,而另一方的饱和度是0。所以,不相交的位置饱和度是不会变的。

(PS->源图图层->实色混合)

2.Mode.LIGHTEN(变亮)

对应算法:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]

只有两张图片重合的区域才会有颜色值的变化,所以只有重合区域才有变亮的效果。对于源图像非重合区域,由于对应区域的目标图像是空白像素,所以直接显示源图像。

(PS->源图图层->变亮)

实例应用中,当选中一本书时,给这本书加上灯光效果,如下图:

public class LightBookView extends View {
    private Paint mBitPaint;
    private Bitmap BmpDST, BmpSRC;
    public LightBookView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBitPaint = new Paint();
        BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.book_bg, null);
        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.book_light, null);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(BmpDST, 0, 0, mBitPaint);
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN);
        canvas.drawBitmap(BmpSRC, 0, 0, mBitPaint);
        mBitPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }
}

(源图)

先把书架作为目标图像画在底层,然后给mBitPaint设置PorterDuffXfermode,最后将源图像覆盖在目标图像上,经过Mode.LIGHTEN的合成,就出现了灯光效果。

3.Mode.DARKEN(变暗)

对应算法如下:

[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]

(PS->源图图层->变暗)

4.Mode.MULTIPLY(正片叠底)

对应算法如下:

[Sa*Da, Sc*Dc]

(PS->源图图层->正片叠底)

与Photoshop不同的是,仅两张图片的相交区域的混合方式与Photoshop中的正片叠底效果是一致的,对于非相交区域则需要根据公式计算。

5.Mode.OVERLAY(叠加)

(PS->源图图层->叠加)

6.Mode.SCREEN(滤色)

对应算法如下:

[Sa + Da - Sa*Da, Sc + Dc - Sc*Dc]

(PS->源图图层->滤色)

到这里,6种颜色相关的混合模式讲完了,总结一下:

(1)这几种模式都是Photoshop中存在的模式,是通过计算改变相交区域的颜色值的。

(2)除了Mode.MULTIPLY(正片叠底)会在目标图像透明时将结果对应区域置为透明,其他图像都不受目标图像透明像素的影响,即源图像的非相交区域保持原样。

(3)在考虑混合模式时,一般只考虑两种:第一,像区域1一样的两个不透明区域的混合;第二,第区域2一样的与完全透明区域的混合。对于与半透明区域的混合,在实战中一般用不到。所以我们在用混合模式时,只需关注区域1和区域2在使用混合模式后的效果即可。

7.示例:Twitter标识的描边效果

在前面学到的几种模式中,只有Mode.MULTIPLY(正片叠底)模式是在两张图片的一方透明时,结果像素是透明的,所以这里使用Mode.MULTIPLY模式。

public class TwitterView extends View {
    private Paint mBitPaint;
    private Bitmap BmpDST, BmpSRC;
    public TwitterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mBitPaint = new Paint();
        BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.twiter_bg, null);
        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.twiter_light, null);
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(BmpDST, 0, 0, mBitPaint);
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
        canvas.drawBitmap(BmpSrc, 0, 0, mBitPaint);
        mBitPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }
}

(源图片)

二、PorterDuffXfermode之源图像模式

1.Mode.SRC

对应算法:[Sa, Sc]

从公式可以看出,在处理源图像所在区域的相交问题时,全部以源图像显示。

源图像矩形是什么透明度就是什么透明度,是什么颜色就是什么颜色。对目标图像圆形不产生影响。

2.Mode.SRC_IN

对应算法:[Sa*Da, Sc*Da]

在公式中,结果值的透明度和颜色值都是由Sa、Sc分别乘以目标图像的Da来计算的。所以,当目标图像为空白像素时,计算结果也将为空白像素。

当源图像矩形Sa=1时,公式=[Da, Sc*Da],即结果是交集部分矩形透明度不变,非交集部分由于目标圆形的透明度为0,所以非交集部分透明度为0,即空白像素。

SRC_IN模式是在相交时利用目标图像的透明度来改变源图像的透明度和饱和度的。当目标图像透明度为0时,源图像就完全不显示。

(图片圆角效果)

小狗图像是源图像,图标图像是一张遮罩图,这张遮罩图的4个角都是圆形切角,而且是透明的。

这里我们就使用了SRC_IN模式的特性:当目标图像与源图像相交时,根据目标图像的透明度来决定显示源图像的哪部分。

所以,在目标图像不透明的位置,源图像就完全显示出来的;而对于目标图像完全透明的4个角,则源图像不显示。这样合成出来的效果就是带有切角的圆形图像。

(图片倒影效果)

当目标图像的透明度在0~255之间时,就会把源图像的透明度和颜色值变小。利用这个特性,可以实现倒影效果。

我们要显示的是小狗的图像,所以源图像是小狗图像,目标图像是一张遮罩图;这张遮罩图是一个从上到下的白色渐变,白色渐变透明度从49%到0。

在结果图中,先画出小狗图像,然后将画布下移,最后将源图像与目标图像再次合成,画出倒影即可。

public class InvertedImageView_SCRIN extends View {
    private Paint mBitPaint;
    private Bitmap BmpDST, BmpSRC, BmpRevert;
    public InvertedImageView_SCRIN(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        mBitPaint = new Paint();
        BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.dog_invert_shade, null);
        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.dog, null);
        Matrix matrix = new Matrix();
        matrix.setScale(1f, -1f);
        BmpRevert = Bitmap.createBitmap(BmpSRC, 0, 0, BmpSRC.getWidth(), BmpSRC.getHeight(), matrix, true);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth()/2;
        int height = width * BmpDST.getHeight()/BmpDST.getWidth();
        // 先画小狗画像
        canvas.drawBitmap(BmpSRC, null, new RectF(0, 0, width, height), mBitPaint);
        // 再将画布下移,画出倒影
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.translate(0, height);
        canvas.drawBitmap(BmpDST, null, new RectF(0, 0, width, height), mBitPaint);
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(BmpRevert, null, new RectF(0, 0, width, height), mBitPaint);
        mBitPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
}

首先利用canvas.drawBitmap(BmpSRC,null,new RectF(0,0,width,height),mBitPaint画出小狗图像,在画图像的同时将源图像缩放到控件宽度为一半;之后将画布下移,根据遮罩和倒置的小狗图像画出小狗倒影即可。

3.Mode.SRC_OUT

对应算法为:[Sa*(1 - Da), Sc*(1 - Da)]

SRC_OUT模式的特性可以概括为:以目标图像的透明度的补值来调节源图像的透明度和饱和度。即当目标图像为空白像素时,就完全显示源图像;当目标图像的不透明度为100%时,相交区域为空白像素。简单来说就是,当目标图像有图像时结果显示空白像素,当目标图像没有图像时结果显示源图像。

当源图像矩形Sa=1时,公式=[1 - Da, Sc*(1 - Da)],即透明度由目标图像圆形决定,Da=1时,交集部分透明度为0,即空白像素,而非交集部分目标图像圆形的Da=0,所以非交集部分的颜色就是Sc即源图像矩形的原本颜色。

实现刮刮卡效果:

Mode.SRC_OUT模式刚好能够实现当目标图像为不透明时,不显示相交区域的源图像;而当目标图像完全透明时,完全显示相交区域的源图像。

public class EraserView_SRCOUT extends View {
    private Paint mBitPaint;
    private Bitmap BmpDST, BmpSRC, BmpText;
    private Path mPath;
    private float mPreX, mPreY;

    public EraserView_SRCOUT(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        mBitPaint = new Paint();
        mBitPaint.setColor(Color.RED);
        mBitPaint.setStyle(Paint.Style.STROKE);
        mBitPaint.setStrokeWidth(45);
        /**
         * If set to a value > 1, requests the decoder to subsample the original image, 
         * returning a smaller image to save memory.
         * The sample size is the number of pixels in either dimension that correspond to 
         * a single pixel in the decoded bitmap.
         * For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, 
         * and 1/16 the number of pixels. Any value <= 1 is treated the same as 1.
         * Note: the decoder uses a final value based on powers of 2, 
         * any other value will be rounded down to the nearest power of 2.
         */
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 2;

        BmpText = BitmapFactory.decodeResource(getResources(), R.drawable.guaguaka_text, null);
        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.dog, options);
        //创建等宽高的空位图
        BmpDST = Bitmap.createBitmap(BmpSRC.getWidth(), BmpSRC.getHeight(), Bitmap.Config.ARGB_8888);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画底图
        /**
        * (Bitmap bitmap, Rect src, Rect dst, Paint paint)
        * Draw the specified bitmap, scaling/translating automatically to fill the destination rectangle. 
        * If the source rectangle is not null, it specifies the subset of the bitmap to draw.
        */
        canvas.drawBitmap(BmpText, null, new RectF(0, 0, BmpDST.getWidth(), BmpDST.getHeight()), mBitPaint);
        /**
         * This behaves the same as save(), but in addition it allocates and redirects drawing to 
         * an offscreen rendering target.
         * All drawing calls are directed to a newly allocated offscreen rendering target.
         * Only when the balancing call to restore() is made, is that offscreen buffer drawn 
         * back to the current target of the Canvas
         * (which can potentially be a previous layer if these calls are nested).
         * Attributes of the Paint - Paint#getAlpha(), Paint#getXfermode(), and Paint#getColorFilter()
         * are applied when the offscreen rendering target is drawn back when restore() is called.
         * (float left, float top, float right, float bottom, Paint paint, int saveFlags)
         * ALL_SAVE_FLAG: Restore everything when restore() is called (standard save flags).
         */
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        //先把手指轨迹画到目标Bitmap上
        Canvas c = new Canvas(BmpDST); // Construct a canvas with the specified bitmap to draw into.
        //借助新画布(new Canvas(BmpDST))完成把路径(mPath)承载画布的BmpDST上
        c.drawPath(mPath, mBitPaint);
        //然后把目标图像画到画布上
        canvas.drawBitmap(BmpDST, 0, 0, mBitPaint);
        //计算源图像区域
        mBitPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
        canvas.drawBitmap(BmpSRC, 0, 0, mBitPaint);

        mBitPaint.setXfermode(null);
        /**
         * @param int saveCount: The save level to restore to.
         * Efficient way to pop any calls to save() that happened after the save count reached saveCount.
         * It is an error for saveCount to be less than 1.
         * Example:
         * int count = canvas.save(); ... // more calls potentially to save()
         * canvas.restoreToCount(count); // now the canvas is back in the same state it was before the initial call to save().
         */
        canvas.restoreToCount(layerId);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.moveTo(event.getX(), event.getY());
                mPreX = event.getX();
                mPreY = event.getY();
                return true;
            case MotionEvent.ACTION_MOVE:
                float endX = (mPreX + event.getX()) / 2;
                float endY = (mPreY + event.getY()) / 2;
                mPath.quadTo(mPreX, mPreY, endX, endY);
                mPreX = event.getX();
                mPreY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        postInvalidate();
        return super.onTouchEvent(event);
    }
}

由于手势轨迹保存在变量mPath中,所以我们必须把它画到空白图像BmpDST上。因为通过Canvas c = new Canvas(BmpDST)创建空白图像BmpDST所对应的画布,这块画布上所画的任意内容都会保存在BmpDST中,所以利用c.drawPath(mPath, mBitPaint)将路径画到BmpDST上。

最后就是根据Xfermode的混合模式来先后画出目标图像和源图像的过程了。只要记得在设置Xfermode之前所画的图像是目标图像,在设置Xfermode之后所画的图像就是源图像,就不会弄错。
由于小狗图像被手势轨迹擦除,从而露出底层的图片,给大家的感觉就是刮刮卡效果。

4.Mode.SRC_OVER

对应的算法为:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc]

在计算结果中,源图像没有改变。它的意思就是,在目标图像的顶部绘制源图像。

源图像矩形Sa=1时,公式=[Sa, Rc=Sc],即合并结果是:矩形透明度和颜色完全保存(包括交集和非交集部分)。

5.Mode.SRC_ATOP

对应算法为:[Da, Sc*Da + (1 - Sa)*Dc]

源图像矩形Sa=1时,公式=[Da, Sc*Da],即合并结果是:交集部分就是矩形的颜色,非交集部分因为目标圆形的Da=0,所以非交集部分为空白像素。

三、目标图像模式与其他模式

1.Mode.DST

计算公式:[Da, Dc]

从公式可以看出,在处理图像所在区域的相交问题时,正好与Mode.SRC模式相反,全部以目标图像显示。

不管源矩形的各种参数,只计算目标圆形的Alpha和Color,即合并后的最终结果是:目标图像圆形是什么透明度就是什么透明度,是什么颜色就是什么颜色。不管与源矩形交集还是非交集部分都直接无视。

2.Mode.DST_IN

计算公式:[Sa*Da, Sa*Dc]

将这个公式与Mode.SRC_IN公式([Sa*Da, Sc*Da])对比一下,发现正好与SRC_IN相反,Mode.DST_IN是在相交时利用源图像的透明度来改变目标图像的透明度和饱和度的。当源图像透明度为0时,目标图像完全不显示。

当源矩形Sa=1时,公式=[Da, Dc],这就和Mode.DST一样了。

利用SRC模式能实现的效果,只需要将源图像和目标图像颠倒,利用对应的DST模式就可以实现同样的效果。

1)示例:区域波纹

        (源图片)

public class CustomView extends View {
    private Paint mPaint;
    private Path mPath;
    private int mItemWaveLength = 1000;
    private int dx;
    private Bitmap BmpSRC, BmpDST;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.text_shade, null);
        BmpDST = Bitmap.createBitmap(BmpSRC.getWidth(), BmpSRC.getHeight(), Bitmap.Config.ARGB_8888);

        startAnim();
    }

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

        generageWavePath();

        //先清空bitmap上的图像,然后再画上Path
        Canvas c = new Canvas(BmpDST);
        c.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
        c.drawPath(mPath, mPaint);

        canvas.drawBitmap(BmpSRC, 0, 0, mPaint);
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(BmpDST, 0, 0, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawBitmap(BmpSRC, 0, 0, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }

    /**
     * 生成此时的Path
     */
    private void generageWavePath() {
        mPath.reset();
        int originY = BmpSRC.getHeight() / 2;
        int halfWaveLen = mItemWaveLength / 2;
        mPath.moveTo(-mItemWaveLength + dx, originY);
        for (int i = -mItemWaveLength; i <= getWidth() + mItemWaveLength; i += mItemWaveLength) {
            mPath.rQuadTo(halfWaveLen / 2, -50, halfWaveLen, 0);
            mPath.rQuadTo(halfWaveLen / 2, 50, halfWaveLen, 0);
        }
        mPath.lineTo(BmpSRC.getWidth(), BmpSRC.getHeight());
        mPath.lineTo(0, BmpSRC.getHeight());
        mPath.close();
    }

    public void startAnim() {
        ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
        animator.setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (Integer) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}

2)示例:区域不规则波纹

        (源图片)

public class CustomView extends View {
    private Paint mPaint;
    private int mItemWaveLength = 0;
    private int dx = 0;
    private Bitmap BmpSRC, BmpDST;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();

        BmpDST = BitmapFactory.decodeResource(getResources(), R.drawable.wave_bg, null);
        BmpSRC = BitmapFactory.decodeResource(getResources(), R.drawable.circle_shape, null);
        mItemWaveLength = BmpDST.getWidth();

        startAnim();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        //先画上圆形
        canvas.drawBitmap(BmpSRC, 0, 0, mPaint);
        //再画上结果
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawBitmap(BmpDST, new Rect(dx, 0, dx + BmpSRC.getWidth(), BmpSRC.getHeight()), 
                          new Rect(0, 0, BmpSRC.getWidth(), BmpSRC.getHeight()), mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawBitmap(BmpSRC, 0, 0, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }

    public void startAnim() {
        ValueAnimator animator = ValueAnimator.ofInt(0, mItemWaveLength);
        animator.setDuration(4000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (Integer) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }
}

3.Mode.DST_OUT

计算公式:[Da*(1 - Sa), Dc*(1 - Sa)]

将这个公式与Mode.SRC_OUT的公式 [Sa*(1 - Da), Sc*(1 - Da)] 对比一下可以看出,Mode.SRC_OUT是利用目标图像的透明度的补值来改变源图像的透明度和饱和度的;而Mode.DST_OUT是通过源图像的透明度的补值来改变目标图像的透明度和饱和度的。
简单来说,在 Mode.DST_OUT模式下,相交区域显示的是目标图像,目标图像的透明度和饱和度与源图像的透明度相反,当源图像的透明度是100%时,则相交区域为空值;当源图像的透明度为0时,则完全显示目标图像。非相交区域完全显示目标图像。

圆形是目标图像,矩形是源图像。是源图像作用于目标图像。

对于公式,当源图像矩形Sa=1时,公式=[0, 0],即最终结果是:交集部分和非交集部分的源矩形都是空白像素。

图中区域1的相交区域:在DST_OUT模式下,由于源图像的透明度是100%,所以计算后的结果图像在这个区域是空白像素。
图中区域2的非相交区域:在DST_OUT模式下,这个区域的源图像透明度仍为100%,所以计算后的结果图像在这个区域仍是空白像素。
所以,当源图像的区域透明度为100%时,所在区域计算结果为透明像素;当源图像的区域透明时,非相交区域的计算结果就是目标图像。

4.Mode.DST_OVER

计算公式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]

同样先与 Mode.SRC_OVER 的公式 [Sa + (1- Sa)*Da, Re= Sc+ (1 - Sa)*Dc]对比一下,可以看出,从SRC模式中以显示SRC图像为主变成了以显示DST图像为主,从SRC模式中由使用目标图像控制结果图像的透明度和饱和度变成了由源图像控制结果图像的透明度和饱和度 。

当源图像矩形Sa=Da=1时,公式=[Sa, Rc=Dc],即矩形的透明度完全由自己决定,作用于交集的颜色就是目标图像圆形的颜色。

到这里,有关DST的模式讲完了,总结一下:

(1)DST相关模式是完全可以使用SRC对应的模式来实现的,只需要将目标图像和源图像对调一下即可。

(2)在SRC模式中,以显示源图像为主,通过目标图像的透明度来调节计算结果的透明度和饱和度;而在DST模式中,以显示目标图像为主,通过源图像的透明度来调节计算结果的透明度和饱和度。

其他模式——Mode.CLEAR:

计算公式:[0, 0]

从公式中可以看到,计算结果直接就是[0, 0],即空白像素。也就是说,源图像所在区域都会变成空白像素,这样就起到了清空源图像所在区域图像的作用。

模式总结:

在实际应用中,我们可以从以下三个方面来决定使用哪种模式。
(1)目标图像和源图像混合,需不需要生成颜色的叠加特效。如果需要,则从颜色叠加相关模式中选择,有Mode.ADD(饱和度相加)、Mode.DARKEN(变暗)、Mode.LIGHTEN(变亮)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加)、Mode.SCREEN(滤色)。
(2)当不需要特效,而需要根据某张图片的透明像素来裁剪时,就需要使用SRC相关模式或DST相关模式了。而SRC 相关模式与DST相关模式是相通的,唯一不同的是决定当前哪个图像是目标图像和源图像。
(3)当需要清空图像时,使用Mode.CLEAR模式。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值