Android自定义控件入门到精通--Xfermode

《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683

《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code

Xfermode

在这里插入图片描述

Xfermode在Android高版本中只保留了一个实现类PorterDuffXfermode,在Android24及以下低版本中,你还可以看到另外两个实现类AvoidXfermode,PixelXorXfermode,高版本已移除了,这里就不展开讲了,其中AvoidXfermode可以用于做选区和选区填充,非常强大,但是直接移除且并没有提供替代方案也是挺无奈的。

PorterDuffXfermode(PorterDuff.Mode mode)

  • PorterDuff.Mode : 混合模式

PorterDuff.Mode

图形混合模式,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff。

在讲ComposeShader的时候,我们已经初步了解过了PorterDuff.Mode

例如,使用Xfermode实现Ps中的正片叠底混合模式效果:

在这里插入图片描述

素材(需要一张桃花图和一张绿蓝渐变图):

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

在混合概念中,有SRC(源)和DST(目标)两个概念,怎么去理解呢?

  • DST:如上面桃花图,在Ps中的下层图层中,它是SRC(源)作用的目标
  • SRC:如上面绿蓝渐变图,在Ps中的上层图层中,它是用来混合的源

记住这句话就行了,谁先画在Canvas上,谁就是DST

@Override
protected void onDraw(Canvas canvas) {
    //加载桃花图片作为dst
    Bitmap dst = BitmapFactory.decodeResource(getResources(), R.mipmap.flower);
    //先将目标dst画到图层上
    canvas.drawBitmap(dst,null,new RectF(0,0,dst.getWidth(),dst.getHeight()),mPaint);
    //设置Xfermode混合模式
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
    //加载渐变颜色图片作为src
    Bitmap src = BitmapFactory.decodeResource(getResources(), R.mipmap.green_blue);
    //再将源图片src画到图层上
    canvas.drawBitmap(src,null,new RectF(0,0,src.getWidth(),src.getHeight()),mPaint);
}

效果:

在这里插入图片描述

可以看到PorterDuff.Mode.MULTIPLY跟Ps中的正片叠底效果是一样的

我们用官方的提供的Demo效果来演示下所有的PorterDuff.Mode效果

https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/graphics/Xfermodes.java

public class TestView extends View {

    private static final int W = 80;
    private static final int H = 80;
    private static final int ROW_MAX = 5;
    private Bitmap mSrcB;
    private Bitmap mDstB;
    private Shader mBG;
    private static final Xfermode[] sModes = {
            new PorterDuffXfermode(PorterDuff.Mode.SRC),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
            new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
            new PorterDuffXfermode(PorterDuff.Mode.DST),
            new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
            new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
            new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
            new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
            new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
            new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
            new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
            new PorterDuffXfermode(PorterDuff.Mode.SCREEN),
            new PorterDuffXfermode(PorterDuff.Mode.ADD),
            new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
            new PorterDuffXfermode(PorterDuff.Mode.XOR),
            new PorterDuffXfermode(PorterDuff.Mode.OVERLAY)
    };
    private static final String[] sLabels = {
            "SRC", "SRC_IN", "SRC_OUT", "SRC_ATOP","SRC_OVER",
            "DST", "DST_IN", "DST_OUT", "DST_ATOP","DST_OVER",
            "DARKEN", "LIGHTEN", "MULTIPLY", "SCREEN","ADD",
            "CLEAR", "XOR", "OVERLAY",
    };

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    private void init() {
        mSrcB = makeSrc(W, H);
        mDstB = makeDst(W, H);

        //创建黑白小块bitmap,用来绘制透明区域的网格示意
        Bitmap bm = Bitmap.createBitmap(new int[]{0xFFFFFFFF, 0xFFCCCCCC,
                        0xFFCCCCCC, 0xFFFFFFFF}, 2, 2,
                Bitmap.Config.RGB_565);
        //通过Shader实现重复填充背景
        mBG = new BitmapShader(bm,
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT);
        Matrix m = new Matrix();
        m.setScale(6, 6);
        mBG.setLocalMatrix(m);
    }

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


    @Override
    protected void onDraw(Canvas canvas) {
        //关闭硬件加速,否则部分mode无效果
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        canvas.drawColor(Color.WHITE);
        Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG);
        labelP.setTextAlign(Paint.Align.CENTER);
        Paint paint = new Paint();
        canvas.translate(15, 35);
        int x = 0;
        int y = 0;
        for (int i = 0; i < sModes.length; i++) {
            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);
            paint.setStyle(Paint.Style.FILL);
            paint.setShader(mBG);
            //填充透明度背景
            canvas.drawRect(x, y, x + W, y + H, paint);
            //保存画布状态
            int sc = canvas.saveLayer(x, y, x + W, y + H, null,Canvas.ALL_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);
            canvas.drawText(sLabels[i],
                    x + W / 2, y+H+20 - labelP.getTextSize() / 2, labelP);
            x += W + 10;
            if ((i % ROW_MAX) == ROW_MAX - 1) {
                x = 0;
                y += H + 50;
            }
        }
    }
}

在这里插入图片描述

第一排:SRC相关

  • SRC:只显示源图像
  • SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】
  • SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤
  • SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响
  • SRC_OVER:将源图像放在目标图像上方

第二排:DST相关

  • DST:只显示目标图像
  • DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响
  • DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤
  • DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响
  • DST_OVER:将目标图像放在源图像上方

第三排:混合效果相关

  • DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合
  • LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关
  • MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值
  • SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖
  • ADD:饱和相加,对图像饱和度进行相加

第四排:归类到第三排(混合效果相关)(囧)

  • CLEAR:清除图像
  • XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制
  • OVERLAY:叠加

每个模式都对应着它自己的算法,比如同个坐标上src 和 dst 两个像素点,颜色值和透明度分别进行加减乘除运算,得到新的像素颜色值,具体算法见下表:

在这里插入图片描述

  • S :SRC的饱和度
  • D :DST的饱和度
  • Sa :SRC的Alpha,同理有Da
  • Sc :SRC的Color值,同理有Dc

可以看到,除了ADD、CLEAR,其它都与透明度有关,例如SRC与SRC的透明度有关,SRC_IN与SRC和DST的透明度都有关,一般都是透明即不过滤,这里要注意一下SRC和DST图层透明区域的影响,这个很重要,这个在Demo中去体会吧。

深入了解请戳:https://developer.android.com/reference/android/graphics/PorterDuff.Mode.html

理解了PorterDuff.Mode就能实现一些好玩的效果

示例一:轨迹截图

功能描述:使用手势轨迹,在一张图片上随意截取想要的部分

path这篇中,我们已经讲过自由绘制路径的效果了,不记得的可以回看一下。

根据理解,我们这个功能应该是使用DST_IN模式,保留与SRC相交部分的内容,全部代码:

public class TestView extends View {

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    private Path mPath;
    private Paint mPaint;
    private Bitmap mBitmap;
    private int w,h;

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#ff0000"));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        mPath = new Path();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_49);
        w=mBitmap.getWidth();
        h=mBitmap.getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        if (userXfermode) {
            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
            canvas.drawBitmap(makeSrc(),0,0, mPaint);
            mPaint.setXfermode(null);
        }else{
            canvas.drawPath(mPath, mPaint);
        }
    }

    //将mPath画在透明图层上,注意透明图层的大小应该和DST图层大小一致
    private Bitmap makeSrc() {
        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(0xFFFF0000);
        p.setStyle(Paint.Style.FILL);
        c.drawPath(mPath, p);
        return bm;
    }

    private float startX, startY;

    private boolean userXfermode;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                userXfermode = false;
                mPath.reset();
                startX = event.getX();
                startY = event.getY();
                mPath.moveTo(startX, startY);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
                startX = event.getX();
                startY = event.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                userXfermode = true;
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }
}

在这里插入图片描述

这个例子就是应用了SRC透明部分内容对应的DST内容会被清除,并保留SRC有内容的部分,类似Ps中的蒙版

示例二:绘画板

上个例子中,我们用DST_IN实现保留于SRC相交的内容,相反的,DST_OUT就可以实现清除与SRC相交的内容了。

绘画板有两个基础功能,绘图和擦除

xml: 自定义View(TestView)作为画板,RadioGroup中两个RadioButton用来切换当前是绘图还是橡皮擦功能

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#222222"
    android:orientation="vertical">

    <cn.code.code.wiget.TestView
        android:id="@+id/testView"
        android:layout_width="match_parent"
        android:layout_height="200dp" />
    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"
        android:orientation="horizontal">
        <RadioButton
            android:id="@+id/paintBtn"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="笔"
            android:button="@null"
            android:textColor="@color/draw_color"
            android:background="#ffffff"
            android:paddingTop="4dp"
            android:checked="true"
            android:gravity="center"
            android:paddingBottom="4dp"
            android:textSize="14sp"
            />
        <RadioButton
            android:id="@+id/clearBtn"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="橡皮擦"
            android:button="@null"
            android:textColor="@color/draw_color"
            android:background="#ffffff"
            android:gravity="center"
            android:paddingTop="4dp"
            android:layout_marginLeft="1dp"
            android:paddingBottom="4dp"
            android:textSize="14sp"
            />
    </RadioGroup>
</LinearLayout>

Activity:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);
    RadioGroup radioGroup=findViewById(R.id.radioGroup);
    final TestView testView=findViewById(R.id.testView);
    radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
            switch (checkedId){
                case R.id.paintBtn:
                    testView.setCurrentStatus(0);
                    break;
                case R.id.clearBtn:
                    testView.setCurrentStatus(1);
                    break;
            }
        }
    });
}

TestView

public class TestView extends View {

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    private Path srcPath, dstPath;
    private Paint mPaint;
    private int width, height;

    private Bitmap mBitmap;

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(0xffff0000);
        srcPath = new Path();
        dstPath = new Path();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //演示效果,宽高都是精确值
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(width, height);
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制白底
        canvas.drawColor(0xffffffff);
        //用mBitmap实例化新画布,这样新的画布上就能有历史内容
        Canvas bitmapCanvas = new Canvas(mBitmap);
        //绘制笔的轨迹作为dst
        bitmapCanvas.drawBitmap(makeDst(), 0, 0, mPaint);
        //设置为DST_OUT,与SRC相交的部分将清空
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        //绘制橡皮擦的轨迹作为SRC
        bitmapCanvas.drawBitmap(makeSrc(), 0, 0, mPaint);
        mPaint.setXfermode(null);
        //将最新的内容更新到画布上
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
    }

    public Bitmap makeDst() {
        Bitmap bm = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(0xff000000);
        p.setStyle(Paint.Style.STROKE);
        p.setStrokeCap(Paint.Cap.ROUND);
        p.setStrokeWidth(5);
        c.drawPath(dstPath, p);
        return bm;
    }

    public Bitmap makeSrc() {
        Bitmap bm = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(0xffff0000);
        p.setStyle(Paint.Style.STROKE);
        p.setStrokeCap(Paint.Cap.ROUND);
        p.setStrokeWidth(30);
        c.drawPath(srcPath, p);
        return bm;
    }


    private float startX, startY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //每次按下都要重置轨迹,以免在已有内容区域重复绘制
                dstPath.reset();
                srcPath.reset();
                startX = event.getX();
                startY = event.getY();
                if (currentStatus == 0) {
                    dstPath.moveTo(startX, startY);
                } else {
                    srcPath.moveTo(startX, startY);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (currentStatus == 0) {
                    dstPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
                } else {
                    srcPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
                }
                startX = event.getX();
                startY = event.getY();
                invalidate();
                break;
        }
        return true;
    }

    private int currentStatus;//0:绘图  1:橡皮擦

    public void setCurrentStatus(int status) {
        currentStatus = status;
    }
}

在这里插入图片描述

示例三:图片上色动画(进度演示?)

在剪映APP导出视频的界面,会有一个视频转换进度的动画效果,我们用Xfermode就可以实现

功能描述:我们用一个去色后的图片作底,给它上色并实现动画效果

public class TestView extends View {

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    private Paint mPaint;
    private Bitmap mBitmap;
    private int w,h;

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#ff0000"));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_49);
        w=mBitmap.getWidth();
        h=mBitmap.getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
        canvas.drawBitmap(makeSrc(),0,0, mPaint);
        mPaint.setXfermode(null);
    }

    //在一个白底图层上画红绿渐变色,这里用白底而非透明或者黑底图层了,跟Ps模板一样,黑遮挡不要的内容,白显示需要的内容
    private Bitmap makeSrc() {
        Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
        Canvas c = new Canvas(bm);
        c.drawColor(0xffffffff);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        LinearGradient linearGradient=new LinearGradient(0,0,w,0,0xffff0000,0xff00ff00, Shader.TileMode.CLAMP);
        p.setShader(linearGradient);
        c.drawRect(0,0,currentX,h, p);
        return bm;
    }

    private int currentX;
    //在Activity中调用这个方法开始动画
    public void startAnimator(){
        ValueAnimator animator=ValueAnimator.ofInt(0,w);
        animator.setDuration(4000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentX= (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }
}

然后添加点击事件,启用动画

findViewById(R.id.testView).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        TestView testView= (TestView) v;
        testView.startAnimator();
    }
});

在这里插入图片描述

Xfermode的脏区及解决办法

啥叫脏区?

我们通过一个小例子来解释下,比如,我们要实现下面这种刮刮卡效果

在这里插入图片描述

示例四:刮刮卡效果

//小知识:每次canvas.drawXXX(),都相当于新建了一个图层,并在新的图层上绘制,参考Ps
@Override
protected void onDraw(Canvas canvas) {
    //绘制一个白底背景
    canvas.drawColor(Color.WHITE);
    //绘制文字图层
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
    //绘制美女图片图层
    canvas.drawBitmap(mDstB,0,0,mPaint);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    //绘制手势轨迹图层
    mPaint.setColor(Color.BLUE);
    mPaint.setStyle(Paint.Style.STROKE);
    canvas.drawPath(mPath,mPaint);
    mPaint.setXfermode(null);
}

按照我们的预期,应该是可以实现刮刮卡效果的

但是:

在这里插入图片描述

美女图片擦除了可以理解,我们的白色底和文字呢?也一起被擦除了?(这个黄底是父View的底色)

在这里插入图片描述

这就是Xfermode的脏区了(我并不想擦除文字和白底这两个图层)

那我们可不可以在当前画布之上在加一个画布,在新的画布上实现刮刮卡的效果,这样就不会影响到文字层的画布了

(后面讲Canvas的时候会细讲)

int saveLayerId = canvas.saveLayer(left, top, right, bottom, mPaint);
......
canvas.restoreToCount(saveLayerId);

所以加上这两行代码,就可以实现刮刮卡效果了,全部代码:

public class TestView extends View {

    private Bitmap mDstB;
    private Paint mPaint;
    private Path mPath;

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

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

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

    private void init() {
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(20);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(30);
        mPath=new Path();
        mDstB = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_48);
        //关闭硬件加速,否则部分mode无效果
        setLayerType(LAYER_TYPE_SOFTWARE,null);
    }


    //小知识:每次canvas.drawXXX(),都相当于新建了一个图层,并在新的图层上绘制,参考Ps
    @Override
    protected void onDraw(Canvas canvas) {
        //绘制一个白底背景
        canvas.drawColor(Color.WHITE);
        //绘制文字图层
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
        //创建一块新的画布区域
        int saveLayerId = canvas.saveLayer(0, 0, mDstB.getWidth(), mDstB.getHeight(), mPaint);
        //绘制美女图片图层
        canvas.drawBitmap(mDstB,0,0,mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        //绘制手势轨迹图层
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mPath,mPaint);
        mPaint.setXfermode(null);
        //回到底层画布上继续其它内容绘制
        canvas.restoreToCount(saveLayerId);
    }

    private float startX, startY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //每次按下都要重置轨迹,以免在已有内容区域重复绘制
                startX = event.getX();
                startY = event.getY();
                mPath.moveTo(startX, startY);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
                startX = event.getX();
                startY = event.getY();
                invalidate();
                break;
        }
        return true;
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一鱼浅游

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

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

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

打赏作者

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

抵扣说明:

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

余额充值