前言:
上一篇android自定义view-打造圆形ImageView(一)中介绍了如何用BitmapShader渲染来绘制圆形圆角ImageView,我们今天采用Xfermode来进行处理,因为相比较而言Xfermode更为常见,更为强大。
知识准备:
我们需要对Xfermode有一定的了解,百度Xfermode,你会看见很多有关它的介绍,我这边就来总结一下。
Xfermode有三个子类 :
public AvoidXfermode(int opColor, int tolerance, Mode mode)AvoidXfermode的构造方法也特别简单,一共接收3个参数:第一个参数opColor是一个16进制的带透明度通道的颜色值,如0X12345678。第二个参数tolerance表示容差值,什么是容差值呢?可以理解成一个表示“精确”和“模糊”的概念,下面会解释一下。第三个参数是AvoidXfermode的模式,AvoidXfermode的模式一共有两种:AvoidXfermode.Mode.TARGET和AvoidXfermode.Mode.AVOID。
public PixelXorXfermode(int opColor)构造方法很简单,只要传递一个16进制带透明通道的颜色值即可,那么这个参数有什么用呢?我在Google文档中,找到了这样的一个算法:实际上PixelXorXfermode内部是按照“opColor ^ src ^ dst”这个异或算法运算的,得到一个不透明的(alpha = 255)的色彩值,设置到图像中
public class XfermodeRoundImageView extends ImageViewStep2:自定义属性
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="borderRadius" format="dimension" /> <attr name="imageType"> <enum name="circle" value="0" /> <enum name="round" value="1" /> </attr> <declare-styleable name="RoundImageView"> <attr name="borderRadius" /> <attr name="imageType" /> </declare-styleable> <declare-styleable name="XfermodeRoundImageView"> <attr name="borderRadius" /> <attr name="imageType" /> </declare-styleable> </resources>Step3:构造方法中初始化值
public XfermodeRoundImageView(Context context) { this(context, null); } public XfermodeRoundImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public XfermodeRoundImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 初始化画笔 mPaint = new Paint(); mPaint.setAntiAlias(true); // 获取自定义属性值 TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XfermodeRoundImageView, defStyle, 0); int count = array.getIndexCount(); for (int i = 0; i < count; i++) { int attr = array.getIndex(i); switch (attr) { case R.styleable.XfermodeRoundImageView_borderRadius: // 获取圆角大小 mBorderRadius = array.getDimensionPixelSize(R.styleable.XfermodeRoundImageView_borderRadius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BORDER_RADIUS_DEFAULT, getResources().getDisplayMetrics())); break; case R.styleable.XfermodeRoundImageView_imageType: // 获取ImageView的类型 type = array.getInt(R.styleable.XfermodeRoundImageView_imageType, TYPE_CIRCLE); break; } } // Give back a previously retrieved StyledAttributes, for later re-use. array.recycle(); }其实这部分和之前没有多大区别,也就没有好讲的了。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 如果是圆形,则强制宽高一致,以最小的值为准 if (type == TYPE_CIRCLE) { int mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight()); setMeasuredDimension(mWidth, mWidth); } }Step5:重写onDraw方法
@Override protected void onDraw(Canvas canvas) { // 从缓存中取出图片 Bitmap bitmap = mWeakBitmap == null ? null : mWeakBitmap.get(); // 如果没有缓存或者被回收了,则重新绘制 if (bitmap == null || bitmap.isRecycled()) { // 获取背景drawable Drawable drawable = getDrawable(); // 如果有背景图则绘制 if (drawable != null) { // 拿到drawable的长度和宽度 int dWidth = drawable.getIntrinsicWidth(); int dHeight = drawable.getIntrinsicHeight(); bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888); // 创建画布 Canvas canvas1 = new Canvas(bitmap); // 设置图片缩放比率 float scale = 1.0f; if (type == TYPE_CIRCLE) { scale = Math.max(getWidth() * 1.0f / dWidth, getHeight() * 1.0f / dHeight); } else { scale = getWidth() * 1.0F / Math.min(dWidth, dHeight); } // 缩放图片 drawable.setBounds(0, 0, (int) (scale * dWidth), (int) (scale * dHeight)); // 绘制DST图片 drawable.draw(canvas1); // 绘制SRC图片 if (mMaskBitmap == null || mMaskBitmap.isRecycled()) { mMaskBitmap = drawType(); } // 重置画笔 mPaint.reset(); // 不采用滤波 mPaint.setFilterBitmap(false); mPaint.setXfermode(xfermode); canvas1.drawBitmap(mMaskBitmap, 0, 0, mPaint); // 绘制处理好的图形 mPaint.setXfermode(null); canvas.drawBitmap(bitmap, 0, 0, mPaint); // drawable.draw(canvas); // 缓存图片 mWeakBitmap = new WeakReference<Bitmap>(bitmap); } } if (bitmap != null) { mPaint.setXfermode(null); canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint); } }这边我们使用到了缓存的知识,我们不会在每次onDraw的时候都会话费内存在处理相同的bitmap上,所以我们使用了WeakReference<T>来方便GC。对了,这边总共有四个引用方便GC,很多面试的时候可能会遇到这样的问题,什么是强引用,什么是弱引用,它们的使用时机与区别,等等之类,有兴趣的去深入了解一下。上面还用到了一个私有方法:
/** * 绘制形状,作为src * * @return */ private Bitmap drawType() { Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); // 创建画笔 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.BLACK); // 如果type为圆形 if (type == TYPE_CIRCLE) { canvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2, paint); } else { canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), mBorderRadius, mBorderRadius, paint); } return bitmap; }用来绘制我们的SRC的bitmap的,也就是我们的形状。
// 在重绘中进行mask和dst的内存回收 @Override public void invalidate() { mWeakBitmap = null; if (mMaskBitmap != null) { mMaskBitmap.recycle(); mMaskBitmap = null; } super.invalidate(); }我们来重点讲一下上面的逻辑。首先我们需要获取缓存的处理好的bitmap,如果缓存好了,就绘制:
if (bitmap != null) { mPaint.setXfermode(null); canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint); }如果说没有缓存的话,那我们就进行处理缓存,并绘制处理好的bitmap。那么,第一步,我们需要获取我们的背景图,如果没有背景图则不处理,有的话就进行图片的的缩放,绘制DST图片。缩放的scale和上一篇是一样的,不过我们这边的缩放不是经过matrix了,而是drawable.setBounds()方法,设置边界值就是相当于缩放图片了,然后在canvas上将DST图片绘制到bitmap上。接下来,我们需要绘制SRC形状图片,这里我们设置画笔mode,将SRC图片绘制到DST同一块画布中,这时的bitmap就是我们处理好的图片了。只需要重置画笔,将处理好的图片绘制出来即可。
package com.beyole.view; import java.lang.ref.WeakReference; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Xfermode; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.widget.ImageView; import com.beyole.roundimageview.R; public class XfermodeRoundImageView extends ImageView { // ImageView类型 private int type; // 圆形图片 private static final int TYPE_CIRCLE = 0; // 圆角图片 private static final int TYPE_ROUND = 1; // 默认圆角宽度 private static final int BORDER_RADIUS_DEFAULT = 10; // 获取圆角宽度 private int mBorderRadius; // 画笔 private Paint mPaint; // 使用缓存机制来保存处理好的bitmap,便于GC private WeakReference<Bitmap> mWeakBitmap; // 设置Xfermode的模式为DST_IN private Xfermode xfermode = new PorterDuffXfermode(Mode.DST_IN); // 蒙板图层 private Bitmap mMaskBitmap; public XfermodeRoundImageView(Context context) { this(context, null); } public XfermodeRoundImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public XfermodeRoundImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 初始化画笔 mPaint = new Paint(); mPaint.setAntiAlias(true); // 获取自定义属性值 TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XfermodeRoundImageView, defStyle, 0); int count = array.getIndexCount(); for (int i = 0; i < count; i++) { int attr = array.getIndex(i); switch (attr) { case R.styleable.XfermodeRoundImageView_borderRadius: // 获取圆角大小 mBorderRadius = array.getDimensionPixelSize(R.styleable.XfermodeRoundImageView_borderRadius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BORDER_RADIUS_DEFAULT, getResources().getDisplayMetrics())); break; case R.styleable.XfermodeRoundImageView_imageType: // 获取ImageView的类型 type = array.getInt(R.styleable.XfermodeRoundImageView_imageType, TYPE_CIRCLE); break; } } // Give back a previously retrieved StyledAttributes, for later re-use. array.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 如果是圆形,则强制宽高一致,以最小的值为准 if (type == TYPE_CIRCLE) { int mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight()); setMeasuredDimension(mWidth, mWidth); } } @Override protected void onDraw(Canvas canvas) { // 从缓存中取出图片 Bitmap bitmap = mWeakBitmap == null ? null : mWeakBitmap.get(); // 如果没有缓存或者被回收了,则重新绘制 if (bitmap == null || bitmap.isRecycled()) { // 获取背景drawable Drawable drawable = getDrawable(); // 如果有背景图则绘制 if (drawable != null) { // 拿到drawable的长度和宽度 int dWidth = drawable.getIntrinsicWidth(); int dHeight = drawable.getIntrinsicHeight(); bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888); // 创建画布 Canvas canvas1 = new Canvas(bitmap); // 设置图片缩放比率 float scale = 1.0f; if (type == TYPE_CIRCLE) { scale = Math.max(getWidth() * 1.0f / dWidth, getHeight() * 1.0f / dHeight); } else { scale = getWidth() * 1.0F / Math.min(dWidth, dHeight); } // 缩放图片 drawable.setBounds(0, 0, (int) (scale * dWidth), (int) (scale * dHeight)); // 绘制DST图片 drawable.draw(canvas1); // 绘制SRC图片 if (mMaskBitmap == null || mMaskBitmap.isRecycled()) { mMaskBitmap = drawType(); } // 重置画笔 mPaint.reset(); // 不采用滤波 mPaint.setFilterBitmap(false); mPaint.setXfermode(xfermode); canvas1.drawBitmap(mMaskBitmap, 0, 0, mPaint); // 绘制处理好的图形 mPaint.setXfermode(null); canvas.drawBitmap(bitmap, 0, 0, mPaint); // drawable.draw(canvas); // 缓存图片 mWeakBitmap = new WeakReference<Bitmap>(bitmap); } } if (bitmap != null) { mPaint.setXfermode(null); canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint); } } /** * 绘制形状,作为src * * @return */ private Bitmap drawType() { Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); // 创建画笔 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.BLACK); // 如果type为圆形 if (type == TYPE_CIRCLE) { canvas.drawCircle(getWidth() / 2, getWidth() / 2, getWidth() / 2, paint); } else { canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), mBorderRadius, mBorderRadius, paint); } return bitmap; } // 在重绘中进行mask和dst的内存回收 @Override public void invalidate() { mWeakBitmap = null; if (mMaskBitmap != null) { mMaskBitmap.recycle(); mMaskBitmap = null; } super.invalidate(); } }activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:beyole="http://schemas.android.com/apk/res/com.beyole.roundimageview" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.beyole.view.XfermodeRoundImageView android:layout_width="200dip" android:layout_height="200dip" android:src="@drawable/demo" beyole:borderRadius="20dip" beyole:imageType="round" /> <com.beyole.view.XfermodeRoundImageView android:layout_width="200dip" android:layout_height="200dip" android:src="@drawable/demo" beyole:borderRadius="20dip" beyole:imageType="circle" /> </LinearLayout>
题外话:
android交流群:279031247(广告勿入)
新浪微博:SmartIceberg