Android自定义控件之CircleImageView

最近写代码需要定义一个显示圆形图片的控件,这种东西网上有很多,千篇一律的方式就是通过Circle和Bitmap取交集的方式,这种方式实现时有严重的锯齿问题;
第二种是通过Paint设置Xfermode来实现的,这种方式可以很好的屏蔽锯齿,但是在使用的过程中定义的Circle必须和要显示的位图一样大小,如果要显示位图的一
部分就不可以了;另外一种认为是比较优化的方式是通过BitmapShaper渲染来实现的,而且绘过程也比较简单;

下面就是我自己实现的CircleImageView,做了进一步的优化:

  • 通过使用Layer解决了Paint设置Xfermode不能绘制图片局部的问题
  • 可以绘制圆角图片
  • 可以选择使用Xfermode或者BitmapShaper的方式来渲染图片

通过测试发现使用Xfermode的方式是比BitmapShaper更优一些,首先是绘制效率,小图没有什么,当大图的时候Xfermode就稍比BitmapShaper快一点,而且使用
Xfermode生成的图片比BitmapShaper生成的图片要小,我测试的过程中,同样的效果图Xfermode生成的图片大小是859kb,而BitmapShaper生成的图片是881kb,
所以我个人比较倾向使用Xfermode的方式,读者可以自己测试以下,下面我们就来看看具体的实现过程吧。

源代码

属性文件配置

attrs.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<declare-styleable name="CircleImageView">
<attr name="radius" format="dimension"/>
<attr name="iscircle" format="boolean"/>
<attr name="cropType">
<flag name="leftTop" value="1"/>
<flag name="leftBottom" value="2"/>
<flag name="rightTop" value="3"/>
<flag name="rightBottom" value="4"/>
<flag name="leftCenter" value="5"/>
<flag name="rightCenter" value="6"/>
<flag name="topCenter" value="7"/>
<flag name="bottomCenter" value="8"/>
<flag name="center" value="9"/>
</attr>
<attr name="drawType">
<flag name="shader" value="1"/>
<flag name="xfermode" value="2"/>
</attr>
</declare-styleable>

CircleImageView

CircleImageView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
public class CircleImageView extends ImageView {
private static final String TAG = "CircleImageView";

private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

/**
* 半径
*/
private int radius = 0;

/**
* 是否是圆形裁剪
* true圈形,false圆角图片
*/
private boolean isCircle = true;

/**
* 裁剪类型
*/
private int cropType = CropType.CENTER;

/**
* 绘制类型
*/
private int drawType = DrawType.XFERMODE;

/**
* 圈形图片的裁剪位置
*/
public static final class CropType {
public static final int LEFT_TOP = 1; //显示图片的左上角部分
public static final int LEFT_BOTTOM = 2;//显示图片的左下角部分
public static final int RIGHT_TOP = 3;//显示图片的右上角部分
public static final int RIGHT_BOTTOM = 4;//显示图片的右下角部分
public static final int LEFT_CENTER = 5;//显示图片的左居中部分
public static final int RIGHT_CENTER = 6;//显示图片的右居中部分
public static final int TOP_CENTER = 7;//显示图片的上居中部分
public static final int BOTTOM_CENTER = 8;//显示图片的下居中部分
public static final int CENTER = 9;//显示图片的居中部分
}

/**
* 使用BitmapShaper方式绘制还是Xfermode的方式绘制
*/
public static final class DrawType {
public static final int SHADER = 1; //使用BitmapShaper方式绘制
public static final int XFERMODE = 2; //使用Xfermode方式绘制
}

public CircleImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray array = null;
try {
array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleImageView, 0, 0);
int count = array.getIndexCount();
Log.d(TAG, "count = " + count);
for (int i = 0; i < count; i++) {
final int index = array.getIndex(i);
if (index == R.styleable.CircleImageView_radius) {
radius = array.getDimensionPixelSize(index, 0); //初始化半径
} else if (index == R.styleable.CircleImageView_cropType) {
cropType = array.getInteger(index, CropType.CENTER); //初始化截取类型
} else if (index == R.styleable.CircleImageView_drawType) {
drawType = array.getInteger(index, DrawType.XFERMODE); //初始化绘制类型
} else if (index == R.styleable.CircleImageView_iscircle) {
isCircle = array.getBoolean(index, true); //是否绘制圆
}
}
Log.d(TAG, "radius = " + radius + " cropType = " + cropType + " drawType = " + drawType + " isCircle = " + isCircle);
} finally {
if (array != null) {
array.recycle();
}
}
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int intrinsicWidth = getDrawable().getIntrinsicWidth();
int intrinsicHeight = getDrawable().getIntrinsicHeight();
Log.d(TAG, "intrinsicWidth = " + intrinsicWidth + " intrinsicHeight = " + intrinsicHeight);
if (isCircle) {
/**
*1、如果圆形半径设置为0,则使用图片中宽高之间的最小值作为圆形的半径
*2、如果圆形半径不为0,则取半径,宽,高之间的最小值作为半径
**/
int width = resolveAdjustedSize(radius == 0 ? intrinsicWidth : Math.min(intrinsicWidth, radius * 2), Integer.MAX_VALUE, widthMeasureSpec);
int height = resolveAdjustedSize(radius == 0 ? intrinsicHeight : Math.min(intrinsicHeight, radius * 2), Integer.MAX_VALUE, heightMeasureSpec);

int border = Math.min(width, height);

radius = border / 2;

Log.d(TAG, "isCircle border = " + border + " radius = " + radius);
setMeasuredDimension(border, border);
} else {
/**
*圆角图片的圆角半径取半径,宽,高之间的最小值
**/
int width = resolveAdjustedSize(intrinsicWidth, Integer.MAX_VALUE, widthMeasureSpec);
int height = resolveAdjustedSize(intrinsicHeight, Integer.MAX_VALUE, heightMeasureSpec);
radius = Math.min(Math.min(width, height), radius);
Log.d(TAG, "isCircle not border = " + Math.min(width, height) + " radius = " + radius);
setMeasuredDimension(width, height);
}
}

/**
*设置半径
**/
public void setRadius(int radius) {
this.radius = radius;
requestLayout();
}

/**
*设置截取类型
**/
public void setCropType(int cropType) {
this.cropType = cropType;
invalidate();
}

/**
*设置绘制类型
**/
public void setDrawType(int drawType) {
this.drawType = drawType;
invalidate();
}

/**
*此方法参考了ImageView的resolveAdjustedSize方法,可以自己查阅
**/
private int resolveAdjustedSize(int desiredSize, int maxSize,
int measureSpec) {
int result = desiredSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
/* Parent says we can be as big as we want. Just don't be larger
than max size imposed on ourselves.
*/
result = Math.min(desiredSize, maxSize);
break;
case MeasureSpec.AT_MOST:
// Parent says we can be as big as we want, up to specSize.
// Don't be larger than specSize, and don't be larger than
// the max size imposed on ourselves.
result = Math.min(Math.min(desiredSize, specSize), maxSize);
break;
case MeasureSpec.EXACTLY:
// No choice. Do what we are told.
result = specSize;
break;
}
return result;
}

@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
if (getDrawable() == null) {
return;
}
if (drawType == DrawType.XFERMODE) {
drawByXfermode(canvas); //使用Xfermode方式绘制
} else {
drawByShader(canvas); //使用Bitmap方式绘制
}
}

private void drawByXfermode(Canvas canvas) {
int width = getWidth();
int height = getHeight();
int restore = canvas.saveLayer(0, 0, width, height, null,
Canvas.ALL_SAVE_FLAG); //保存Layer
if (isCircle) {
canvas.drawCircle(radius, radius, radius, paint); //绘制圆形
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //设置Xfermode
canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG); //二次保存Layer
Bitmap bitmap = drawableToBitmap(getDrawable());
int[] xy = getCropTypeCircleXY(bitmap); //获取图片中显示圆形的中心坐标
Rect src = new Rect(); //定义设置源图片的显示区域
src.left = xy[0] - radius;
src.right = xy[0] + radius;
src.top = xy[1] - radius;
src.bottom = xy[1] + radius;
canvas.drawBitmap(bitmap, src, new Rect(0, 0, width, height), null); //绘制图片
} else {
RectF rect = new RectF(0, 0, width, height);
canvas.drawRoundRect(rect, radius, radius, paint); //绘制圆角矩形
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//设置Xfermode
canvas.saveLayer(0, 0, width, height, paint, Canvas.ALL_SAVE_FLAG);//二次保存Layer
Bitmap bitmap = drawableToBitmap(getDrawable());
canvas.drawBitmap(bitmap, 0, 0, null);
}
canvas.restoreToCount(restore); //恢复Layer
paint.setXfermode(null);
}

private void drawByShader(Canvas canvas) {
Bitmap src = drawableToBitmap(getDrawable());
if (isCircle) {
int[] cropTypeCircleXY = getCropTypeCircleXY(src);//获取图片中显示圆形的中心坐标
Bitmap bitmap = Bitmap.createBitmap(src, cropTypeCircleXY[0] - radius, cropTypeCircleXY[1] - radius, getWidth(), getHeight()); //创建显示区域的图片
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); //设置BitmapShader
paint.setShader(bitmapShader);
canvas.drawCircle(radius, radius, radius, paint);
} else {
BitmapShader bitmapShader = new BitmapShader(src, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(bitmapShader);
int width = getWidth();
int height = getHeight();
RectF rect = new RectF(0, 0, width, height);
canvas.drawRoundRect(rect, radius, radius, paint);
}
}

/**
*将drawable转换成bitmap
**/
private Bitmap drawableToBitmap(Drawable drawable) {
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.draw(canvas);
return bitmap;
}

/**
*获得显示区域的圆形坐标
**/
private int[] getCropTypeCircleXY(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] xy;
switch (cropType) {
case CropType.LEFT_TOP:
xy = new int[]{radius, radius};
break;
case CropType.LEFT_BOTTOM:
xy = new int[]{radius, height - radius};
break;
case CropType.RIGHT_TOP:
xy = new int[]{width - radius, radius};
break;
case CropType.RIGHT_BOTTOM:
xy = new int[]{width - radius, height - radius};
break;
case CropType.LEFT_CENTER:
xy = new int[]{radius, height / 2};
break;
case CropType.RIGHT_CENTER:
xy = new int[]{width - radius, height / 2};
break;
case CropType.TOP_CENTER:
xy = new int[]{width / 2, radius};
break;
case CropType.BOTTOM_CENTER:
xy = new int[]{width / 2, height - radius};
break;
case CropType.CENTER:
xy = new int[]{width / 2, height / 2};
break;
default:
xy = new int[]{width / 2, height / 2};
break;
}
return xy;
}
}

使用方法

显示的xml
1
2
3
4
5
6
7
8
9
10
<com.think.android.widget.CircleImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/circleimageview_demo"
android:background="@android:color/darker_gray"
app:radius="60dp"
app:cropType="leftTop"
app:drawType="shader"/>

本文[示例代码]


参考资料
CircleImageView-方式2