自定义 View——PorterDuffXfermode
楔子 我们在自定义的过程,经常会遇到多个图形相交的问题(如下图) ,那么系统是如何处理图 片相交部分的绘制的呢?
View 的基本框架(之后的代码都是基于该 View): public class PorterDuffXfermodeView extends View { //画笔 priv
ate final Paint mPaint = new Paint(); //View 的宽高 private int mViewWidth; private int mViewHeight; public PorterDuffXfermodeView(Context context) {
this(context,null); } public PorterDuffXfermodeView(Context context, AttributeSet attrs) { this(context, attrs,0); } public PorterDuffXfermodeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initWidget(); } private void initWidget(){ //初始化画笔 mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); mPaint.setDither(true); }
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mViewWidth = w; mViewHeight = h; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } } 实现代码: //自定义 View @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(mViewWidth/2,mViewHeight/2); firstExample(canvas); } /**
* 制作具有相交部分的圆和正方形 */ private void firstExample(Canvas canvas){ //绘制一个正方型 mPaint.setColor(Color.BLUE); canvas.drawRect(-300,-300,0,0,mPaint); //绘制一个圆 mPaint.setColor(Color.RED); canvas.drawCircle(0,0,200,mPaint); } 首先我们要知道系统是如何绘制图形的,系统每绘制一个图形(如:canvas.drawXxx())就 代表创建了一个图层,那么什么叫做图层? 我们看一张图就明白了
1、图片中的三个颜色分别代表一个图层,每个图层的内容就是绘制的图形。 2、系统默认是图层向上累加绘制,也就是红色是最先绘制的,蓝色是之后绘制的,黄色是 最后绘制的。所以图形相交的部分,就被上层的图层的内容给掩盖了。 既然明白了图层的概念,我们就可以回到正题,如何处理图层的相交部分。Android 为我们 提供了 PorterDuffXfermode 这个类,来处理关于图层相交的问题。 PorterDuffXfermode 的使用 PorterDuffXfermode 能够实现的功能 首先我们来看一下 PorterDuffXfermode 有哪些功能 注:(dst 表示先绘制的图,src 表示后绘制的图)
稍微简单的解释一下常用的功能: 凡是带有 IN 的表示:取两个图层的相交部分,关于相交部分显示什么内容有 DST 和 SRC 决定。 凡是带有 OUT 的表示:取两个图层中另一方不相交的部分。 凡是带有 OVER 的表示:当俩个图层存在相交部分时,显示哪个图层的内容 PorterDuffXfermode 的简单使用 作用一:图层的交换 任务:将第一幅图的正方形显示在顶部,圆形显示在底部。 首先如何创建 PorterDuffXfermode。 //构造方法 /** * PorterDuff.Mode:这是一个枚举类,枚举类的参数为上面的示意图
*/ PorterDuffXfermode(PorterDuff.Mode mode) 然后,将创建好的 PorterDuffXfermode 放入 Paint 中 mPaint.setXfermode(new PorterDuffXfermode(mode)) 为什么是将模式放入到画笔中 0 0,这是表示画笔当遇到图形相交的问题的时候,按照这种 方式来解决。 代码的实现: private void secondExample(Canvas canvas){ //绘制一个正方型 mPaint.setColor(Color.BLUE); canvas.drawRect(-300,-300,0,0,mPaint); //设置相交时候,图层显示的模式(表示相交部分显示前一个图层的内容) mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); //绘制一个圆 mPaint.setColor(Color.RED); canvas.drawCircle(0,0,200,mPaint); } 效果图 :
我们发现,竟然红色的圆消失了 - -,什么鬼跟说好的不一样呀! ! 。 原因是这种直接使用 PorterDuffXfermode 的用法是错误的- -,真是糟糕的体验,哪里错了, 原理上完全没问题好吧。 (警告:PorterDuffXfermode 的第一道坑) 正确的使用方法:首先需要创造一个 Bitmap,然后首先在这个 Bitmap 上绘制完成之后,再 将这个 Bitmap 通过 canvas 显示在 View 上。 正确代码: private void thirdExample(Canvas canvas){ //创建一个 Bit Bitmap out = Bitmap.createBitmap(600,600, Bitmap.Config.ARGB_8888); //创建该 Bitmap 的画布 Canvas bitmapCanvas = new Canvas(out); //绘制一个正方型
mPaint.setColor(Color.BLUE); bitmapCanvas.drawRect(0,0,300,300,mPaint); //设置相交时候,图层显示的模式(表示当相交的时候,圆形为先绘制的图形) mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); //绘制一个圆 mPaint.setColor(Color.RED); bitmapCanvas.drawCircle(300,300,200,mPaint); //最后,将完成的图片绘制在 View 上 canvas.drawBitmap(out,-300,-300,null); } 效果图:
天哪,真是太麻烦了- - ,直接让圆形和方形的绘制顺序调换一下,才是最快最省时间的方 法 ~~。 当然对与这个例子通过调换执行顺序是最快的, 但是毕竟我们是来学习使用技巧的。 实现圆框图片
作用二:实现两张图相交部分显示的效果 效果图:
原理说明: 首先创建一个圆形, 这个圆形是用来设置图片显示的区域。 之后获取图片资源 (图 片的大小最好比设定的圆形大) 。然后,设置 Xfermode 为 SRC_IN 就表示,当圆形和图片相 交的时候,截取相交部分(也就是截取圆形) ,相交部分显示后一个图层的内容(也就是图 片) 代码展示: private void forthExample(Canvas canvas){ //创建自定义的 Bitmap Bitmap out = Bitmap.createBitmap(300,300, Bitmap.Config.ARGB_8888); //获取图片 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.picture); //获取 Bitmap 的画笔 Canvas bitmapCanvas = new Canvas(out);
//在 Bitmap 上绘制一个圆形 bitmapCanvas.drawCircle(150,150,150,mPaint); //设置显示后画图形的交集 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //绘制需要显示的图片 bitmapCanvas.drawBitmap(bitmap,0,0,mPaint); canvas.drawBitmap(out,-150,-150,null); } 补充:图层的合并 在制作刮刮卡我们还需要补充一个知识, 在开始的位置我曾讲到绘制一次图形就相当于创建 一个图层。
其实实际上还有一个步骤,当使用 canvas 完成一次绘制过程后,就会将当前图层,和上一 个图层合并为一个图层。也就是说,我们首先创建了一个图层,并在其上画了个正方形,然 后我们又创建了一个图层,并在其上绘制了一个圆形。当圆形绘制完成的时候,两个图层合 并成为了一个图层。
那么这有什么后果呢? 合并之后就表示, 相交部分不再是红色覆盖在蓝色上的上下层关系。 而是相交部分是红色代 替了。 如果不合并图层的话,那么我创建了第三个图层,是圆的内接矩形,并使用 DST_OUT(表 示挖出红色圆的内接矩形)那么之后,倒影出得应该是这样的图形。
代码: private void fifthExample(Canvas canvas){ //首先在 View 上绘制正方形 mPaint.setColor(Color.BLUE); canvas.drawRect(-300,-300,0,0,mPaint); //创建 Bitmap Bitmap out = Bitmap.createBitmap(400,400, Bitmap.Config.ARGB_8888); Canvas bitmapCanvas = new Canvas(out); //在 Bitmap 上绘制圆 mPaint.setColor(Color.RED); bitmapCanvas.drawCircle(200,200,200,mPaint); //设置模式为 DST_OUT mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); //圆的内接正方形的参数 float squareLeft = 200 - 100 * (float)Math.sqrt(2); float squareTop = squareLeft;
float squareRight = squareLeft + 200 * (float)Math.sqrt(2); float squareBottom = squareRight; // 取 圆 与 内 接 正 方 形 不 相 交 的 部 分 bitmapCanvas.drawRect(squareLeft,squareTop,squareRight,squareBottom,mPaint); //绘制在 View 上 canvas.drawBitmap(out,-200,-200,null); } 但是实际上是这样的:如果是三个图层的话,第二个第三个图层合并之后,透明部分 显示应该有第一个图层的内容。但是由于图层的合并,之后就没有层次这个概念了,最后透 明部分显示的就是 View 的背景 效果图:
private void sixthExample(Ca canvas){ Bitmap out = Bitmap.createBitmap(500,500, Bitmap.Config.ARGB_8888); Canvas bitmapCanvas = new Canvas(out); //在 Bitmap 上绘制正方形(这是与上面代码区别的部分)
mPaint.setColor(Color.BLUE); bitmapCanvas.drawRect(0,0,250,250,mPaint); mPaint.setColor(Color.RED); bitmapCanvas.drawCircle(250,250,200,mPaint); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); float squareLeft = 250 - 100 * (float)Math.sqrt(2); float squareTop = squareLeft; float squareRight = squareLeft + 200 * (float)Math.sqrt(2); float squareBottom = squareRight; bitmapCanvas.drawRect(squareLeft,squareTop,squareRight,squareBottom,mPaint); canvas.drawBitmap(out,-200,-200,null); } 大招:实现刮刮卡的效果 效果图:
原理:首先获取遮盖的图片,之后再创建遮盖在图片上的蒙版。最后通过点击事件, 设置擦除的路径,制作出路径和蒙版合并的遮罩层。
代码: public class ScratchView extends View { private final Paint mPaint = new Paint(); private Bitmap mContentBitmap; private Bitmap mMaskBitmap; private Canvas mBitmapCanvas; private final Path mPath = new Path(); private int mViewWidth; private int mViewHeight; private int mBitmapWidth; private int mBitmapHeight; public ScratchView(Context context) { this(context,null); } public ScratchView(Context context, AttributeSet attrs) { this(context, attrs,0); } public ScratchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initWidget(); } private void initWidget(){ //初始化画笔 mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(20); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setColor(Color.TRANSPARENT); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //初始化,刮刮卡的内容 mContentBitmap = BitmapFactory. decodeResource(getResources(), R.mipmap.picture);
mBitmapWidth = mContentBitmap.getWidth(); mBitmapHeight = mContentBitmap.getHeight(); //初始化刮刮卡的遮盖效果 mMaskBitmap = Bitmap.createBitmap(mBitmapWidth,mBitmapHeight, Bitmap.Config.ARGB_8888); mBitmapCanvas = new Canvas(mMaskBitmap); //设置灰色的遮盖层 mBitmapCanvas.drawColor(Color.GRAY); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mViewWidth = w; mViewHeight = h; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制图片 canvas.drawBitmap(mContentBitmap,0,0, null); //绘制蒙版 canvas.drawBitmap(mMaskBitmap,0,0,null); } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: mPath.moveTo(x,y); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(x,y); break; case MotionEvent.ACTION_UP: mPath.lineTo(x,y); break; }
//设置擦除的路径 mBitmapCanvas.drawPath(mPath,mPaint); //重绘 invalidate(); return true; } } 但是我们知道在自定义 View 中不光有 canvas,还有 path,如何在 Path 类上,使用类似 集合这样的功能呢?