Android绘图篇(六)——setXfermode

上一篇:Android绘图篇(五)——setShader 设置着色器

上篇总结了Paint中的setShader方法,这篇来总结下setXfermode方法。setXfermode需要传入一个Xfermode对象。来看下这个类的介绍:


Android绘图篇(六)——setXfermode_第1张图片

它是一种图像混合模式,当然只知道这个,并不能理解它是干嘛的,也不知道怎么用,不过没关系,通过一个个例子就能逐渐体会它的奥妙。至于使用的时候,只需要传入其子类就行了,它共有三个子类,不过AvoidXfermode和PixelXorXfermode已经被逐渐废弃了,这里就不浪费篇幅去介绍了,我们主要使用PorterDuffXfermode。好了,既然是图像混合模式,那就看看它是怎么混合的呗:


Android绘图篇(六)——setXfermode_第2张图片

这是google官方在PorterDuffXfermode类介绍页面的一张图片,已经被所有写这类博客的人用烂了,于是乎我也放了一张。图像混合,既然是混合,一定有两张图像啊,黄色的我们称之为目标图像,也叫Dst,蓝色的我们称之为源图像,也叫Src,先明确这两个最基本的概念,图像混合就是Src和Dst之间通过指定一种混合的模式,来决定两者混合后的样子。也就是说Src和Dst之间通过指定混合模式,混合可以产生这么多种效果。

想要使用Paint 的setXfermode这个方法,最好关闭硬件加速,关闭硬件加速有以下几种方法:
1、在AndroidManifest中的Appilcation节点中配置android:hardwareAccelerated=“false”:


Android绘图篇(六)——setXfermode_第3张图片

这是application层面的关闭,所有的Activity、View都被关闭了硬件加速,但有的时候还是需要用到硬件加速的,因此不推荐这种方法。

2、在Activity节点中配置android:hardwareAccelerated=“false”:


Android绘图篇(六)——setXfermode_第4张图片

这种方式只会在Acivity层面关闭硬件加速,该Acivity所在的界面关闭。

3、在Activity代码中配置如下代码:

getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

也是Activity层面的关闭。

4、View级别关闭硬件加速

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

这样只针对某个控件关闭硬件加速。

根据需求决定如何使用即可,带过一下就行了。

好吧,看下PorterDuffXfermode的构造方法:

public PorterDuffXfermode(PorterDuff.Mode mode)

有且只有一个参数PorterDuff.Mode,其有以下这些取值:

Mode.CLEAR  
Mode.SRC  
Mode.DST  
Mode.SRC_OVER  
Mode.DST_OVER  
Mode.SRC_IN  
Mode.DST_IN  
Mode.SRC_OUT  
Mode.DST_OUT  
Mode.SRC_ATOP  
Mode.DST_ATOP  
Mode.XOR  
Mode.DARKEN  
Mode.LIGHTEN  
Mode.MULTIPLY  
Mode.SCREEN  
Mode.OVERLAY  
Mode.ADD  

每个模式都对应一种算法。


Android绘图篇(六)——setXfermode_第5张图片

要了解该算法,得先了解每种公式中元素的具体含义:

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的值。

首先我们准备两个图形,一个正方形一个圆形,就像google给的图那样,两者的位置也像图中那样;我们再不设置任何混合模式的情况下绘制两个图形:

public class PictureView extends View {
    private Paint mPaint;
    private int mWidth = 400;
    private int mHeight = 400;


    public PictureView(Context context) {
        super(context);
        init();
    }

    public PictureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PictureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(getDest(mWidth, mHeight), 0, 0, mPaint);
        canvas.drawBitmap(getSrc(mWidth, mHeight), mWidth / 2, mWidth / 2, mPaint);

    }

    public Bitmap getDest(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        //目标图像橘色
        paint.setColor(Color.parseColor("#FF8C00"));
        canvas.drawCircle(width / 2, height / 2, width / 2, paint);
        return bitmap;

    }

    public Bitmap getSrc(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        //源图像绿色
        paint.setColor(Color.parseColor("#006400"));
        canvas.drawRect(0, 0, 400, 400, paint);
        return bitmap;

    }
}

emmm~


Android绘图篇(六)——setXfermode_第6张图片

这里是通过在Bitmap中绘制图形,形成Bitmap,然后再将Bitmap绘制出来。然后我们来使用图像混合模式再试一次,在用这个之前,我们需要在新的图层上绘制:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int layId = canvas.saveLayer(0, 0, mWidth, mHeight, mPaint);
        
        //关键代码写在这中间。
 
        canvas.restoreToCount(layId);
    }

至于为什么,后面会专门写一篇去介绍Canvas图层相关的知识,先这样写就行了。
然后我们采用混合模式中的SRC_IN这个模式:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //新图层
        int layId = canvas.saveLayer(0, 0, mWidth, mHeight, mPaint);

        canvas.drawBitmap(getDest(mWidth, mHeight), 0, 0, mPaint);
        
        //设置混合模式为SRC_IN
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        canvas.drawBitmap(getSrc(mWidth, mHeight), mWidth / 2, mWidth / 2, mPaint);

        canvas.restoreToCount(layId);
        
    }

然后你会发现:


Android绘图篇(六)——setXfermode_第7张图片

咦,你会发现,和下面这个图不一样啊:


在这里插入图片描述

按照google给的图,我们应该会绘制出这样一个扇形,颜色是绿色的呀,可是为什么黄色部分还在呢?google又是怎么实现这样的效果呢?不急,关于每个类,google都提供了一个一个Demo,关于Xfermode,google也提供了一个Demo类供我们学习,所以说官方的Demo是最好的学习手段,这个类就是Xfermodes.java,路径在:D:\Android\samples\android-14\ApiDemos\src\com\example\android\apis\graphics下。我们来看看Xfermodes.java的源码是什么样的:

public class Xfermodes extends GraphicsActivity {

    static 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*3/4, h*3/4), p);
        return bm;
    }

    // create a bitmap with a rect, used for the "src" image
    static 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(w/3, h/3, w*19/20, h*19/20, p);
        return bm;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SampleView(this));
    }

    private static class SampleView extends View {
        private static final int W = 64;
        private static final int H = 64;
        private static final int ROW_MAX = 4;   // number of samples per row

        private Bitmap mSrcB;
        private Bitmap mDstB;
        private Shader mBG;     // background checker-board pattern

        private static final Xfermode[] sModes = {
            new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
            new PorterDuffXfermode(PorterDuff.Mode.SRC),
            new PorterDuffXfermode(PorterDuff.Mode.DST),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
            new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
            new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
            new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
            new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
            new PorterDuffXfermode(PorterDuff.Mode.XOR),
            new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
            new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
            new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
            new PorterDuffXfermode(PorterDuff.Mode.SCREEN)
        };

        private static final String[] sLabels = {
            "Clear", "Src", "Dst", "SrcOver",
            "DstOver", "SrcIn", "DstIn", "SrcOut",
            "DstOut", "SrcATop", "DstATop", "Xor",
            "Darken", "Lighten", "Multiply", "Screen"
        };

        public SampleView(Context context) {
            super(context);

            mSrcB = makeSrc(W, H);
            mDstB = makeDst(W, H);

            // make a ckeckerboard pattern
            Bitmap bm = Bitmap.createBitmap(new int[] { 0xFFFFFFFF, 0xFFCCCCCC,
                                            0xFFCCCCCC, 0xFFFFFFFF }, 2, 2,
                                            Bitmap.Config.RGB_565);
            mBG = new BitmapShader(bm,
                                   Shader.TileMode.REPEAT,
                                   Shader.TileMode.REPEAT);
            Matrix m = new Matrix();
            m.setScale(6, 6);
            mBG.setLocalMatrix(m);
        }

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

            Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG);
            labelP.setTextAlign(Paint.Align.CENTER);

            Paint paint = new Paint();
            paint.setFilterBitmap(false);

            canvas.translate(15, 35);

            int x = 0;
            int y = 0;
            for (int i = 0; i < sModes.length; i++) {
                // draw the border
                paint.setStyle(Paint.Style.STROKE);
                paint.setShader(null);
                canvas.drawRect(x - 0.5f, y - 0.5f,
                                x + W + 0.5f, y + H + 0.5f, paint);

                // draw the checker-board pattern
                paint.setStyle(Paint.Style.FILL);
                paint.setShader(mBG);
                canvas.drawRect(x, y, x + W, y + H, paint);

                // draw the src/dst example into our offscreen bitmap
                int sc = canvas.saveLayer(x, y, x + W, y + H, null,
                                          Canvas.MATRIX_SAVE_FLAG |
                                          Canvas.CLIP_SAVE_FLAG |
                                          Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                                          Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                                          Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                canvas.translate(x, y);
                canvas.drawBitmap(mDstB, 0, 0, paint);
                paint.setXfermode(sModes[i]);
                canvas.drawBitmap(mSrcB, 0, 0, paint);
                paint.setXfermode(null);
                canvas.restoreToCount(sc);

                // draw the label
                canvas.drawText(sLabels[i],
                                x + W/2, y - labelP.getTextSize()/2, labelP);

                x += W + 10;

                // wrap around when we've drawn enough for one row
                if ((i % ROW_MAX) == ROW_MAX - 1) {
                    x = 0;
                    y += H + 30;
                }
            }
        }
    }
}

导包啥的这里就省略了。这是一个Activity,setContentView()设置的是一个自定义View SampleView,SampleView中定义了mSrcB,mDstB分别代表源图和目标图像,和我们定义的是一样意思。主要在SapmleView的onDraw方法中有一个for循环,在for循环中有这样一段代码:

int sc = canvas.saveLayer(x, y, x + W, y + H, null,
                                          Canvas.MATRIX_SAVE_FLAG |
                                          Canvas.CLIP_SAVE_FLAG |
                                          Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                                          Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                                          Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                canvas.translate(x, y);
                canvas.drawBitmap(mDstB, 0, 0, paint);
                paint.setXfermode(sModes[i]);
                canvas.drawBitmap(mSrcB, 0, 0, paint);
                paint.setXfermode(null);
                canvas.restoreToCount(sc);

是不是跟我们刚写的很像?对的,这就是用来绘制图像混合后的效果的。为啥要用for循环呢?也不难理解,google给的图是这样的:
Android绘图篇(六)——setXfermode_第8张图片

那肯定要遍历所有的混合模式,把所有的效果都绘制出来啊,所以才需要for循环,而我们只需要看PorterDuff.Mode.SRC_IN的效果,那就可以改造一下:

public class Xfermodes extends GraphicsActivity {

    static 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*3/4, h*3/4), p);
        return bm;
    }

    static 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(w/3, h/3, w*19/20, h*19/20, p);
        return bm;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SampleView(this));
    }

    private static class SampleView extends View {
        private static final int W = 64;
        private static final int H = 64;

        private Bitmap mSrcB;
        private Bitmap mDstB;


        public SampleView(Context context) {
            super(context);

            mSrcB = makeSrc(W, H);
            mDstB = makeDst(W, H);
        }

        @Override protected void onDraw(Canvas canvas) {
                int sc = canvas.saveLayer(0, 0,  W, y , null,);
                canvas.drawBitmap(mDstB, 0, 0, paint);
                paint.setXfermode(PorterDuff.Mode.SRC_IN);
                canvas.drawBitmap(mSrcB, 0, 0, paint);
                paint.setXfermode(null);
                canvas.restoreToCount(sc);
        }
    }
}

把乱七八糟的都去掉,我们发现原来google绘制的矩形和圆形根本就不是按照我们想象的位置去绘制的。真实的情况如下:


Android绘图篇(六)——setXfermode_第9张图片

图中mSrcB代表源图像,mDstB代表目标图像,你可以很明显的发现, 绘制的圆形的半径并不是等于bitmap的宽度/2,同样绘制的矩形的大小也并不等于bitmap的大小 。这里其实两个bitmap是完全重合的,只是为了分析其坐标,我把它拆分出来了,大家可以动用空间想象能力来把mSrcB和mDstB完全重合,看能得到什么图形。既然如此,我们也仿照google安卓给的坐标点,改造下我们自己的程序:

public class PictureView extends View {
    private Paint mPaint;
    private int mWidth = 400;
    private int mHeight = 400;


    public PictureView(Context context) {
        super(context);
        init();
    }

    public PictureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PictureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setAntiAlias(true);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //新图层
        int layId = canvas.saveLayer(0, 0, mWidth, mHeight, mPaint);

        canvas.drawBitmap(getDest(mWidth, mHeight), 0, 0, mPaint);

        //设置混合模式为SRC_IN
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        canvas.drawBitmap(getSrc(mWidth, mHeight), 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(layId);

    }

    public Bitmap getDest(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        //目标图像橘色
        paint.setColor(Color.parseColor("#FF8C00"));
        canvas.drawOval(new RectF(0, 0, mWidth * 3 / 4, mWidth * 3 / 4), paint);
        return bitmap;

    }

    public Bitmap getSrc(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        //源图像绿色
        paint.setColor(Color.parseColor("#006400"));
        canvas.drawRect(mWidth / 3, mWidth / 3, mWidth * 19 / 20, mWidth * 19 / 20, paint);
        return bitmap;

    }
}

主要区别就这两行代码:

canvas.drawOval(new RectF(0, 0, mWidth*3/4, mWidth*3/4), paint);

canvas.drawRect(mWidth/3, mWidth/3, mWidth*19/20, mWidth*19/20, paint);

就是绘制圆和绘制矩形时的坐标点都改成官方demo提供的计算方式,然后我们再来看:


Android绘图篇(六)——setXfermode_第10张图片

再来对比一下官方的:


在这里插入图片描述

是不是一模一样,当然大小不一样哈。这里是google给我们的误导,我们看google给我们的图,会想当然的计算出矩形的坐标和圆形的坐标,然而实际上google画图的坐标是什么3/4,19/20之类的,这尼玛谁能想到啊,对此我只想对google说:


Android绘图篇(六)——setXfermode_第11张图片

我总结这个的时候,也给我带来很大的困扰,因为按照我们自己臆想的坐标,死活跑不出来google的效果,最后也是看到有一篇博客介绍说源码里面的坐标有猫腻,最后一看果真如此。这也印证了如果你和谷爹跑出来的效果不一样,不用说,一定是你的问题,如何去解决,就可以去查看官方Demo的源码,找出其中和你不同的地方,一定会有所发现的。

好了,效果已经和谷爹的效果一样了,我们来通过SRC_IN的公式来解释一下:

[Sa * Da, Sc * Da]  

这是SRC_IN模式下的图形混合的计算公式,会在两个图像的所在的每个像素都会运用这个算法去计算最终的颜色值。我们来看下不设置混合模式下的效果:


Android绘图篇(六)——setXfermode_第12张图片

混合后的图像大体可以分成三个部分:

  • 圆形且和矩形不相交的部分,比如其中的A点。
  • 矩形和圆形相交的部分,比如其中的B点。
  • 矩形和圆形不相交的部分,例如其中的C点。

我们举这三个点来举例,如果这三个点都满足上述的公式,那就证明这个公式是正确的。于是:
1、在A点,Sa=0,因为Src压根就没绘制到A所在的点啊,所以当然为0,所以公式的前半部分Alpha通道Sa * Da=0;同理后面的Sc * Da=0,于是公式计算的结果是[0,0],所以最终什么都不显示,A点如此,那么所有圆形且和矩形不相交的部分都不显示。

2、在B点,Da=1,因为B点在圆形内,所以目标图像的Alpha通道为1,于是Sa * Da=Sa,Sc * Da=Sc,计算结果就是[Sa,Sc],就是Src的颜色值,所以显示绿色。同理,所有相交的部分都显示绿色。

3、C点和A点同理,只不过换成Da=0,最终的计算结果也是[0,0],所以自然啥也不显示咯。

通过上面的运算过程,我们可以得出一个结论,在SRC_IN模式下:
两者不相交的部分显示空白,两者相交的部分显示Src的颜色,不过最终的颜色受到Dst的透明度影响。因为[Sa * Da, Sc * Da] 公式中都乘了一个系数Da呀。

这个结论和我们最终运行出来的效果并不相悖,所以结论是正确的。好吧,下面过一下其中常用的几种模式的计算。有了SRC_IN模式下的详细计算,其它的我会直接根据公式写出结论,就不一一去推导了,走你~

Src
[Sa, Sc] :只显示源图像,这个好理解:


Android绘图篇(六)——setXfermode_第13张图片

Dst
[Da, Dc] :只显示目标图像


Android绘图篇(六)——setXfermode_第14张图片

SRC_OVER

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

同样运用三点法分析下:
1、在A点,Sa=0,于是计算结果是[Da,Dc],就显示Dst的颜色。
2、在B点,Sa=1,计算结果是[Sa,Sc],所以显示Src的颜色。
3、在C点,Sa=1,计算结果和2是一样的,所以也显示Src的颜色。

于是得出结论:两者不相交的位置显示该位置原有的颜色,相交的位置显示Src的颜色,但是最终颜色的计算会受到Da,即目标图像的Alpha通道的影响。


Android绘图篇(六)——setXfermode_第15张图片
** DST_OVER**

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

分析方式同SRC_OVER一样。


Android绘图篇(六)——setXfermode_第16张图片

SRC_IN
分析过,略。。。

DST_IN

分析同SRC_IN


Android绘图篇(六)——setXfermode_第17张图片

SRC_OUT

[Sa * (1 - Da), Sc * (1 - Da)]  

结论:Dst中和Src不相交的部分显示空白;Dst和Src相交的部分也显示空白,Src中和Dst不相交的部分显示Src的颜色值,会受到Dst的透明度的影响


Android绘图篇(六)——setXfermode_第18张图片

DST_OUT

[Da * (1 - Sa), Dc * (1 - Sa)]  

和SRC_OUT相反


Android绘图篇(六)——setXfermode_第19张图片

SRC_ATOP

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

Dst中和Src不相交的部分显示Dst的色值;两者相交的部分计算结果为[Da,Sc],Dst的透明度和Src的颜色值合成最终的显示颜色。;Src中和Dst不相交的部分计算结果为[Da,0],显示空白


Android绘图篇(六)——setXfermode_第20张图片

DST_ATOP

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

和SRC_ATOP相反


Android绘图篇(六)——setXfermode_第21张图片

XOR

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

结论,Dst中和Src不相交的部分计算结果是[Da,Dc],所以显示Dst中的颜色;两者相交的部分如果完全不透明,则计算结果为[0,0],显示空白;Src中和Dst不相交的部分计算结果是[Sa,Sc],所以显示Src中的颜色


Android绘图篇(六)——setXfermode_第22张图片

DARKEN

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

两者不相交的部分会显示各自的Dst或Src图像,相交的部分类似遮盖变暗效果,但是实际运行出来的效果却不一样:


Android绘图篇(六)——setXfermode_第23张图片

不知道啥原因,很困惑,知道的小伙伴可以留言告诉我一下,感激不尽~

LIGHTEN
两者不相交的部分会显示各自的Dst或Src图像,相交的部分类变亮,同样也没运行出一样的效果:


Android绘图篇(六)——setXfermode_第24张图片

心累~

MULTIPLY

[Sa * Da, Sc * Dc]  

不相交的部分计算结果为[0,0],所以只会显示两者相交的部分,相交部分的透明度和颜色值都是两者的乘积。

Android绘图篇(六)——setXfermode_第25张图片

SCREEN

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

两者不相交部分显示各自的颜色,相交的部分按照公式计算出最终的透明度和颜色并显示:


Android绘图篇(六)——setXfermode_第26张图片

好了,差不多就这么多种模式,最后再举两个小例子,说说setXfermode在Android中的应用。

setXfermode在Android的应用

1. 利用setXfermode实现圆形头像
假设我们提供的图片是一张方形的圆形图片,想要获得一张圆形的图片,只需要裁剪一下就好了,如果我们应用setXfermode来做,就很容易分析到使用方形的图片作为Src图片,一张透明的圆图像作为Dst,然后运用SRC_IN的模式来混合两张图像,最后就可以得到一个圆形、内容是Src图片中内容的圆形头像。原始头像:


Android绘图篇(六)——setXfermode_第27张图片

在init方法中初始化bitmap:

 photo = BitmapFactory.decodeResource(getResources(), R.drawable.people);

Dst图像用圆形空白图像:

public Bitmap getDest(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        canvas.drawCircle(width/2,height/2,150,paint);
        return bitmap;
    }

采用SRC_IN模式绘制:

  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //新图层
        int layId = canvas.saveLayer(0, 0, photo.getWidth(), photo.getHeight(), mPaint);

        canvas.drawBitmap(getDest(photo.getWidth(), photo.getHeight()), 0, 0, mPaint);

        //设置混合模式为SRC_IN
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        canvas.drawBitmap(photo, 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(layId);

    }

结果:


Android绘图篇(六)——setXfermode_第28张图片

不过圆形头像的半径是150写死的,我们需要找出原始头像的最大内切圆,所以稍微改进下:

canvas.drawCircle(width/2,height/2,Math.min(width/2,height/2),paint);

在绘制Dst圆形时,半径取宽和高一半中的最小值,这样绘制出来的圆形是原始头像的内切圆,能保证圆形头像的最大化:


Android绘图篇(六)——setXfermode_第29张图片

2. 利用setXfermode实现橡皮擦效果
橡皮擦是什么效果,就是随着手指的移动,经过的地方都变成空白,翻译过来就是两者相交的地方显示空白,那么我们使用SRC_OUT模式可以实现这个效果。我们来分析下如何实现:
手指滑动,肯定会有一个路径,我们想到了Path,每次移动手指的时候都更新Path,然后再Dst空白图中绘制更新的Path,此时Dst中就不再是透明的了,而是白色的手指轨迹。然后再Src和Dst之间用SRC_OUT模式混合,相交的地方就可以变成空白了。好了,原理说完了,代码也很简单:

public class PictureView extends View {
    private Paint mPaint;
    private Bitmap photo;
    private Bitmap dstBimtp;
    private Path path;


    public PictureView(Context context) {
        super(context);
        init();
    }

    public PictureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PictureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(15);
        mPaint.setAntiAlias(true);

        //初始化路径
        path = new Path();

        //Src图像
        photo = BitmapFactory.decodeResource(getResources(), R.drawable.people);

        //Dst图像
        dstBimtp = getDest(photo.getWidth(), photo.getHeight());

    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //新图层
        int layId = canvas.saveLayer(0, 0, photo.getWidth(), photo.getHeight(), mPaint);

        //每次重绘时先在Dst中把白色的手指轨迹绘制出来
        Canvas dstCanvas = new Canvas(dstBimtp);
        dstCanvas.drawPath(path, mPaint);


        canvas.drawBitmap(dstBimtp, 0, 0, mPaint);

        //设置混合模式为SRC_IN
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));

        canvas.drawBitmap(photo, 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(layId);

    }

    public Bitmap getDest(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        return bitmap;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                //手指移动时更新路径
                path.lineTo(event.getX(), event.getY());
                break;

        }
        //重绘
        postInvalidate();
        return true;
    }
}

结果嘛:


Android绘图篇(六)——setXfermode_第30张图片

好了,到这里功能基本上就实现了。不过还有个瑕疵,就是绘制出来的手指的轨迹不圆滑,有些坑坑洼洼的,于是我们可以用贝塞尔曲线来优化一下,贝塞尔曲线就是专门用来干这个事的。如果不知道贝塞尔曲线的,可以看一下我之前的文章: Android绘图篇(三)——绘制Path路径及贝塞尔曲线

主要修改的代码在onTouchEvent中:

public class PictureView extends View {
    private Paint mPaint;
    private Bitmap photo;
    private Bitmap dstBimtp;
    private Path path;
    private float startX;
    private float startY;


    public PictureView(Context context) {
        super(context);
        init();
    }

    public PictureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PictureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(15);
        mPaint.setAntiAlias(true);

        //初始化路径
        path = new Path();

        //Src图像
        photo = BitmapFactory.decodeResource(getResources(), R.drawable.people);

        //Dst图像
        dstBimtp = getDest(photo.getWidth(), photo.getHeight());

    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //新图层
        int layId = canvas.saveLayer(0, 0, photo.getWidth(), photo.getHeight(), mPaint);

        //每次重绘时先在Dst中把白色的手指轨迹绘制出来
        Canvas dstCanvas = new Canvas(dstBimtp);
        dstCanvas.drawPath(path, mPaint);


        canvas.drawBitmap(dstBimtp, 0, 0, mPaint);

        //设置混合模式为SRC_IN
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));

        canvas.drawBitmap(photo, 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(layId);

    }

    public Bitmap getDest(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        return bitmap;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                path.moveTo(startX, startY);
                break;
            case MotionEvent.ACTION_MOVE:
                //手指移动时更新路径
                float endX = (event.getX() - startX) / 2 + startX;
                float endY = (event.getY() - startY) / 2 + startY;
                path.quadTo(startX, startY, endX, endY);

                startX = event.getX();
                startY = event.getY();
                break;

        }
        //重绘
        postInvalidate();
        return true;
    }
}

好了,然后我们在看下效果:


Android绘图篇(六)——setXfermode_第31张图片

你会发现你绘制的手指的轨迹会圆滑很多。

好了,图形混合总结到这里就差不多了,我们知道了每种图形混合的效果,但是实际的使用中,如何去精准的选择一种图形混合模式呢?

实用经验:
在实际应用中,我们可以从下面三个方面来决定使用哪一个模式:
1、首先,目标图像和源图像混合,需不需要生成颜色的叠加特效,如果需要叠加特效则从颜色叠加相关模式中选择,有Mode.ADD(饱和度相加)、Mode.DARKEN(变暗),Mode.LIGHTEN(变亮)、Mode.MULTIPLY(正片叠底)、Mode.OVERLAY(叠加),Mode.SCREEN(滤色)

2、当不需要特效,而需要根据某一张图像的透明像素来裁剪时,就需要使用SRC相关模式或DST相关模式了。由于SRC相关模式与DST相关模式是相通的,唯一不同的是决定当前哪个是目标图像和源图像;

3、当需要清空图像时,使用Mode.CLEAR。

差不多就这样吧,具体使用的时候再看也没关系,谁能记住这么多种模式的效果-。-
文中有不正确的地方还请看到的小伙伴留言给我,感激不尽,完~

你可能感兴趣的:(Android自定义控件)