前言:

上一篇android自定义view-打造圆形ImageView(一)中介绍了如何用BitmapShader渲染来绘制圆形圆角ImageView,我们今天采用Xfermode来进行处理,因为相比较而言Xfermode更为常见,更为强大。

知识准备:

我们需要对Xfermode有一定的了解,百度Xfermode,你会看见很多有关它的介绍,我这边就来总结一下。

Xfermode有三个子类 :

  1. AvoidXfermode 指定了一个颜色和容差,强制Paint避免在它上面绘图(或者只在它上面绘图)。
  2. PixelXorXfermode 当覆盖已有的颜色时,应用一个简单的像素异或操作。
  3. PorterDuffXfermode 这是一个非常强大的转换模式,使用它,可以使用图像合成的16条Porter-Duff规则的任意一条来控制Paint如何与已有的Canvas图像进行交互。

Part1:AvoidXfermode


AvoidXfermode不支持硬件加速,在高于API16的机器上不会适用,如果想测试这个子类


  1. 可以关闭手机的硬件加速模块。
  2. 在AndroidManifest.xml中Application节点上设置硬件加速为false。

来看一下构造方法:



public AvoidXfermode(int opColor, int tolerance, Mode mode)

AvoidXfermode的构造方法也特别简单,一共接收3个参数:第一个参数opColor是一个16进制的带透明度通道的颜色值,如0X12345678。第二个参数tolerance表示容差值,什么是容差值呢?可以理解成一个表示“精确”和“模糊”的概念,下面会解释一下。第三个参数是AvoidXfermode的模式,AvoidXfermode的模式一共有两种:AvoidXfermode.Mode.TARGET和AvoidXfermode.Mode.AVOID。




Part2:PixelXorXfermode 


PixelXorXfermode是Xfermode下的另外一种图像混排模式,该类特别简单,不过呢,也很不幸的,在API16中已经过时了。我们来做一个简单的了解,先看PixelXorXfermode的构造方法:


public PixelXorXfermode(int opColor)

构造方法很简单,只要传递一个16进制带透明通道的颜色值即可,那么这个参数有什么用呢?我在Google文档中,找到了这样的一个算法:实际上PixelXorXfermode内部是按照“opColor ^ src ^ dst”这个异或算法运算的,得到一个不透明的(alpha = 255)的色彩值,设置到图像中


Part3:PorterDuffXfermode


对于这个经常用的类,我只想用官方的一张图说明即可。




android imageview 切圆角 android设置圆形imageview_android


大家一定要好好看这张图,总共有16种模式。我还是贴一下这16种模式的解释吧,以后还可以回顾参考一下。


从上面我们可以看到PorterDuff.Mode为枚举类,一共有16个枚举值:
1.PorterDuff.Mode.CLEAR
所绘制不会提交到画布上。
2.PorterDuff.Mode.SRC
显示上层绘制图片
3.PorterDuff.Mode.DST
显示下层绘制图片
4.PorterDuff.Mode.SRC_OVER
正常绘制显示,上下层绘制叠盖。
5.PorterDuff.Mode.DST_OVER
上下层都显示。下层居上显示。
6.PorterDuff.Mode.SRC_IN
取两层绘制交集。显示上层。
7.PorterDuff.Mode.DST_IN
取两层绘制交集。显示下层。
8.PorterDuff.Mode.SRC_OUT
取上层绘制非交集部分。
9.PorterDuff.Mode.DST_OUT
取下层绘制非交集部分。
10.PorterDuff.Mode.SRC_ATOP
取下层非交集部分与上层交集部分
11.PorterDuff.Mode.DST_ATOP
取上层非交集部分与下层交集部分
12.PorterDuff.Mode.XOR
异或:去除两图层交集部分
13.PorterDuff.Mode.DARKEN
取两图层全部区域,交集部分颜色加深
14.PorterDuff.Mode.LIGHTEN
取两图层全部,点亮交集部分颜色
15.PorterDuff.Mode.MULTIPLY
取两图层交集部分叠加后颜色
16.PorterDuff.Mode.SCREEN
取两图层全部区域,交集部分变为透明色


正文:


知识准备已经全部讲好了,我们今天采用的是DST_IN模式。接下来开始我们的代码部分,先把效果图奉上吧:



android imageview 切圆角 android设置圆形imageview_android_02



Step1:继承ImageView


public class XfermodeRoundImageView extends ImageView

Step2:自定义属性


我这边是在上一篇的基础上写的,所以,自定义的属性是在attrs.xml的基础加了一个属性


<?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();
	}

其实这部分和之前没有多大区别,也就没有好讲的了。


Step4:重写onMeasure方法


@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的,也就是我们的形状。

最后别忘了,在绘制view的时候手动GC一下。


// 在重绘中进行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就是我们处理好的图片了。只需要重置画笔,将处理好的图片绘制出来即可。


完整代码:


XfermodeRoundImageView.java:


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>


github地址: https://github.com/xuejiawei/beyole_roundimageview 欢迎fork or star