1. 概述
在一些项目中要求将头像显示成圆形或者其他的一些不规则形状的图形,我们不可能为了实现这样的效果,在代码中将图像进行裁剪,这样的话也显得太low了,也没有扩展性。一般实现自定义形状的图形有三种方式:PorterDuffXfermode 、BitmapShader、ClipPath。下面我都会分别说明,首先看一下实现效果图:
2. PorterDuffXfermode 方式
这是由Tomas Proter和 Tom Duff命名的图像转换模式,它有16个枚举值来控制在Canvas上两个图层的叠加结果(先画的图层在下层)。
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 取两图层全部区域,交集部分变为透明色
1> 实现思路
会玩Ps的朋友肯定知道,如果有两个图层,我们想把上面图层裁切成下面图层的形状,只需要调下面图层的选区,然后选中上面的图层,蒙板就可以了。那么我们就可以利用PorterDuff.Mode的 SRC_IN 或 DST_IN 来取得两个图层的交集,从而把图像裁切成我们想要的各种样式。我们需要一个形状图层和一个显示图层。并且显示图层要完全覆盖形状图层。
2> 代码实现与详解
我是通过自定义View实现(直接继承View)的,并且通过自定义属性来设置形状图层和显示图层,自定义属性相关的代码如下:
public AvatarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.AvatarView);
shapeLayerDrawable = ta.getDrawable(R.styleable.AvatarView_av_shape_layer);
showLayerDrawable = ta.getDrawable(R.styleable.AvatarView_av_show_layer);
ta.recycle();
}
在onLayout方法中获取AvatarView的宽高并且对形状图层和显示图层区交集,实现代码如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
bitmap = createImage();
}
// 利用PorterDuff.Mode的 SRC_IN 或 DST_IN 取形状图层和显示图层交集,从而得到自定义形状的图片
private Bitmap createImage() {
Bitmap shapeLayerBitmap = getBitmapFromDrawable(shapeLayerDrawable);
Bitmap showLayerBitmap = getBitmapFromDrawable(showLayerDrawable);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
Bitmap finalBmp = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(finalBmp);
if (null != shapeLayerBitmap) {
shapeLayerBitmap = getCenterInsideBitmap(shapeLayerBitmap, mViewWidth, mViewHeight);
canvas.drawBitmap(shapeLayerBitmap, 0, 0, paint);
}
if (null != showLayerBitmap) {
showLayerBitmap = getCenterCropBitmap(showLayerBitmap, mViewWidth, mViewHeight);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(showLayerBitmap, 0, 0, paint);
}
return finalBmp;
}
/**
* 类比ScaleType.CENTER_INSIDE
*/
private Bitmap getCenterInsideBitmap(Bitmap src, float sideLength) {
float srcWidth = src.getWidth();
float srcHeight = src.getHeight();
float scaleWidth = 0;
float scaleHeight = 0;
if (srcWidth > srcHeight) {
scaleWidth = sideLength;
scaleHeight = (sideLength / srcWidth) * srcHeight;
} else if (srcWidth < srcHeight) {
scaleWidth = (sideLength / srcHeight) * srcWidth;
scaleHeight = sideLength;
} else {
scaleWidth = scaleHeight = sideLength;
}
return Bitmap.createScaledBitmap(src, (int) scaleWidth, (int) scaleHeight, false);
}
/**
* 类比ScaleType.CENTER_INSIDE
*/
private Bitmap getCenterInsideBitmap(Bitmap src, float rectWidth, float rectHeight) {
float srcRatio = ((float) src.getWidth()) / src.getHeight();
float rectRadio = rectWidth / rectHeight;
if (srcRatio < rectRadio) {
return getCenterInsideBitmap(src, rectHeight);
} else {
return getCenterInsideBitmap(src, rectWidth);
}
}
/**
* 类比ScaleType.CENTER_CROP
*/
private Bitmap getCenterCropBitmap(Bitmap src, float rectWidth, float rectHeight) {
float srcRatio = ((float) src.getWidth()) / src.getHeight();
float rectRadio = rectWidth / rectHeight;
if (srcRatio < rectRadio) {
Bitmap scaledBitmap = Bitmap.createScaledBitmap(src, (int) rectWidth, (int) ((rectWidth / src.getWidth()) * src.getHeight()), false);
return Bitmap.createBitmap(scaledBitmap, 0, (int) ((scaledBitmap.getHeight() - rectHeight) / 2), (int) rectWidth, (int) rectHeight);
} else {
Bitmap scaledBitmap = Bitmap.createScaledBitmap(src, (int) ((rectHeight / src.getHeight()) * src.getWidth()), (int) rectHeight, false);
return Bitmap.createBitmap(scaledBitmap, (int) ((scaledBitmap.getWidth() - rectWidth) / 2), 0, (int) rectWidth, (int) rectHeight);
}
}
/**
* Drawable转Bitmap
*/
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (OutOfMemoryError e) {
return null;
}
}
得到自定义形状的图片后,就是通过重写onDraw方法进行绘制:
@Override
protected void onDraw(Canvas canvas) {
if (null != bitmap) {
canvas.drawBitmap(bitmap, 0, 0, null);
}
}
然后就是在布局中使用了,下面是上图中左上角的第一张图片的布局:
上面的custom:av_shape_layer定义的就是形状图层,它是用xml定义的图形,代码如下:
注意:这种方式有一个缺陷,比如通过RecylerView实现一个列表,列表的每一个item都是用圆角图片做背景,在配置比较低的手机上滑动RecylerView时就会发现有点卡顿,这是在主线程上做耗时操作导致的(也就是上面代码中的createImage比较耗时),因此这种方式不推荐使用,推荐使用下面将要讲解的两张方式。
3. BitmapShader方式
关于如何使用BitmapShader,大家可以参考Drawable绘制过程源码分析和自定义Drawable实现动画中关于Shader的讲解,这里就不再赘叙了,通过这种方式实现上图中第一行第二个图片的关键代码如下:
// 初始化BitmapShader
if (mBitmap != null) {
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
} else {
mBitmapShader = null;
}
// 创建圆角矩形path
roundedPath = new Path();
roundedPath.addRoundRect(new RectF(0, 0, mViewWidth, mViewHeight),
new float[]{topLeftRadius, topLeftRadius, topRightRadius, topRightRadius,
bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius},
Path.Direction.CW);
// 将mBitmapShader设置到画笔中
mPaint.setShader(mBitmapShader);
// 在画布上进行绘制
if (mPaint.getShader() == null) {
canvas.drawBitmap(mBitmap, 0, 0, null);
} else {
canvas.drawPath(roundedPath, mPaint);
}
上面是通过BitmapShader绘制圆角图片的核心代码,有兴趣的同学可以自己实现一个自定义View试试看。
4. ClipPath方式
上面的方式实现起来还是有些复杂,因此ClipPath方式是我的首选,
以下是通过ClipPath方式实现圆角图片的代码:
public class RoundedImageView extends ImageView {
/**
* view 的宽度
*/
private int mViewWidth;
/**
* view 的高度
*/
private int mViewHeight;
/**
* view四个圆角对应的半径大小
*/
private float topLeftRadius = 0;
private float topRightRadius = 0;
private float bottomLeftRadius = 0;
private float bottomRightRadius = 0;
private Path roundedPath = null;
public RoundedImageView(Context context) {
this(context,null,0);
}
public RoundedImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundedImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
super.setScaleType(ScaleType.CENTER_CROP);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Room_List_RoundedImageView);
if (ta.hasValue(R.styleable.Room_List_RoundedImageView_room_list_riv_radius)) {
float radius = ta.getDimensionPixelSize(R.styleable.Room_List_RoundedImageView_room_list_riv_radius, 0);
if (radius >= 0) {
topLeftRadius = radius;
topRightRadius = radius;
bottomLeftRadius = radius;
bottomRightRadius = radius;
}
return;
}
topLeftRadius = ta.getDimensionPixelSize(R.styleable.Room_List_RoundedImageView_room_list_riv_topLeftRadius, 0);
topRightRadius = ta.getDimensionPixelSize(R.styleable.Room_List_RoundedImageView_room_list_riv_topRightRadius, 0);
bottomLeftRadius = ta.getDimensionPixelSize(R.styleable.Room_List_RoundedImageView_room_list_riv_bottomLeftRadius, 0);
bottomRightRadius = ta.getDimensionPixelSize(R.styleable.Room_List_RoundedImageView_room_list_riv_bottomRightRadius, 0);
ta.recycle();
}
public void setRadius(float radius) {
if (radius >= 0) {
topLeftRadius = radius;
topRightRadius = radius;
bottomLeftRadius = radius;
bottomRightRadius = radius;
invalidate();
}
}
public void setRadius(float topLeftRadius, float topRightRadius, float bottomLeftRadius, float bottomRightRadius) {
if (topLeftRadius >= 0) {
this.topLeftRadius = topLeftRadius;
}
if (topRightRadius >= 0) {
this.topRightRadius = topRightRadius;
}
if (bottomLeftRadius >= 0) {
this.bottomLeftRadius = bottomLeftRadius;
}
if (bottomRightRadius >= 0) {
this.bottomRightRadius = bottomRightRadius;
}
invalidate();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mViewWidth = getWidth();
mViewHeight = getHeight();
updateRoundedPath();
}
private void updateRoundedPath() {
roundedPath = new Path();
roundedPath.addRoundRect(new RectF(0, 0, mViewWidth, mViewHeight),
new float[]{topLeftRadius, topLeftRadius, topRightRadius, topRightRadius,
bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius},
Path.Direction.CW);
}
@Override
protected void onDraw(Canvas canvas) {
if (null != roundedPath) {
canvas.clipPath(roundedPath);
}
super.onDraw(canvas);
}
}
上面的代码很简单,通过圆角矩形的Path对画布进行裁剪。对于其它形状的图形,通过Path也是都可以实现的。