Android中实现圆形图片,总的说来有2种方法。
- Xfermode方式,就是本文要讲的实现方式。
- BitmapShader(着色器)和Matrix(矩阵)方式
第二种实现方式的代表作,就是Henning Dodenhof的开源项目https://github.com/hdodenhof/CircleImageView,会在下一篇文章中对其分析。
Android 圆形图片开源项目CircleImageView源码分析
废话不多说,先上图看效果。
从图一中,可以看到思路:
1、从图片正中心切一个最大的正方形。
2、然后把这个正方形变成圆形。
3、(可选)加边框。
下面我们就一步一步来实现圆形图片。
自定义attr文件
在values文件夹下新建attr.xml文件,文件代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleImageView">
<attr name="border_width" format="dimension" />
<attr name="border_corlor" format="color" />
</declare-styleable>
</resources>
在自定义属性中,我们定义了边框的宽度和边框的颜色。
新建CircleImageView类
新建的CircleImageView继承自ImageView,类中变量和构造函数如以下代码:
private static final int DEFAULT_BORDER_WIDTH = 0;
private static final int DEFAULT_BORDER_COLOR = Color.WHITE;
private int borderWidth = DEFAULT_BORDER_WIDTH;
private int borderColor = DEFAULT_BORDER_COLOR;
private Paint paint;
public CircleImageView(Context context) {
this(context, null);
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
borderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH);
borderColor = typedArray.getColor(R.styleable.CircleImageView_border_corlor, DEFAULT_BORDER_COLOR);
typedArray.recycle();
init();
}
在类中,定义了2个int变量,用来保存从属性中获取的边框宽度和边框颜色,定义了一支画笔用于绘图。
在init函数中,对画笔进行初始化。
private void init() {
paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
}
到此,初始化工作就算完成了。初始化完成之后,我们就按照思路,一步一步来实现圆形图片。
从图片正中心切一个最大的正方形
代码如下:
/**
* 把矩形图片裁剪成正方形图片
* @param bitmap
* @return
*/
private Bitmap squareBitmap(Bitmap bitmap) {
Bitmap squareBitmap = null;
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
int squareSide = 0;
int x = 0, y = 0;
if(bitmapHeight > bitmapWidth) {
squareSide = bitmapWidth;
y = (bitmapHeight - bitmapWidth) / 2;
squareBitmap = Bitmap.createBitmap(bitmap, x, y, squareSide, squareSide);
}
else if(bitmapHeight < bitmapWidth) {
squareSide = bitmapHeight;
x = (bitmapWidth - bitmapHeight) / 2;
squareBitmap = Bitmap.createBitmap(bitmap, x, y, squareSide, squareSide);
}
else {
squareBitmap = bitmap;
}
return squareBitmap;
}
代码中,我们先获取到了图片的宽和高。
比较图片的宽和高那个小,取小的作为正方形的边长。
然后调用Bitmap.createBitmap函数,从原图中生成一张正方形的图片。
y = (bitmapHeight - bitmapWidth) / 2;
x = (bitmapWidth - bitmapHeight) / 2;
这两句代码,用于保证获取到的是从图片中心生成的最大的正方形。
把正方形图片变成圆形
代码如下:
/**
* 把矩形图片转换成圆形图片
* @param bitmap 原始图片最好为正方形,否则会因为拉伸而失真。
* @return
*/
private Bitmap circleBitmap(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Bitmap circleBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas canvas = new Canvas(circleBitmap);
Rect rect = new Rect(0, 0, width, height);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
canvas.drawCircle(width / 2.0f, height / 2.0f, width / 2.0f, paint);
paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return circleBitmap;
}
这个函数,是整个圆形图片实现的核心代码。
Xfermode方式也是在这里应用。
我们先创建一块画布和一个矩形区域。
然后在画布上画一个圆,圆的直径,就是正方形图片的边长。
接下来,这句很重要的代码:
paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
最后,我们把正方形图片画上去。
经过这3步之后,我们就会得到一个圆形图片。
为什么经过这3步之后,就能得到一个圆形图片呢?
在画布上画图,画一次是一层,再画一次又是一层。
所以,我们画一个圆和一张图片,就是画了2层。
这2层是有交集的,交集就是圆,这个圆,就是我们要的形状。
那内容怎么来呢?
我们要取的是正方形图片的内容,就用到了Xfermode的功能。
paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
这一句代码中的SRC_IN模式,就决定了,我们取到的是圆交集中图片的内容。
关于Xfermode各种模式的效果,如下图所示:
关于Xfermode的资料,可以参考http://www.cnblogs.com/jacktu/archive/2012/01/02/2310326.html
拿到了圆形图片,那剩下的工作就是在onDraw中,把图片画出来。
覆写onDraw
代码如下:
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
if (getWidth() == 0 || getHeight() == 0) {
return;
}
if (drawable.getClass() == NinePatchDrawable.class) {
super.onDraw(canvas);
return;
}
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
Bitmap squareBitmap = squareBitmap(bitmap);
// 处理padding
int defaultWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int defaultHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int scaleSize = Math.min(defaultWidth, defaultHeight);
float top = getPaddingTop() + (defaultHeight - scaleSize) / 2.0f;
float left = getPaddingLeft() + (defaultWidth - scaleSize) / 2.0f;
// temp 用来处理边框与图像的边缘
int temp = 0;
if(borderWidth > 0)
temp = 1;
bitmap = Bitmap.createScaledBitmap(squareBitmap, scaleSize - borderWidth * 2 + temp, scaleSize - borderWidth * 2 + temp, true);
Bitmap circleBitmap = circleBitmap(bitmap);
canvas.drawBitmap(circleBitmap, borderWidth - temp / 2.0f + left, borderWidth - temp / 2.0f + top, paint);
drawCircleBorder(canvas, scaleSize / 2.0f + left, scaleSize / 2.0f + top, scaleSize / 2.0f);
}
我们调用getDrawable获得drawable对象,然后从drawable对象中获得原始的bitmap对象。
然后把原始bitmap切成正方形。
切成正方形之后,经过一次缩放,使图片符合我们的大小,代码如下:
bitmap = Bitmap.createScaledBitmap(squareBitmap, scaleSize - borderWidth * 2 + temp, scaleSize - borderWidth * 2 + temp, true);
得到合适的正方形后,再把正方形变成圆形。
其中一些代码,用来处理padding值,和计算坐标的,大家自行去理解。
以下代码,是用于处理边框与图片边缘误差问题:
// temp 用来处理边框与图像的边缘
int temp = 0;
if(borderWidth > 0)
temp = 1;
最后一句代码,用于话边框。
drawCircleBorder(canvas, scaleSize / 2.0f + left, scaleSize / 2.0f + top, scaleSize / 2.0f);
画边框
代码如下:
/**
* 画边框
* @param canvas
* @param x
* @param y
* @param radius
*/
private void drawCircleBorder(Canvas canvas, float x, float y, float radius) {
if(borderWidth <= 0)
return;
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(borderColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(borderWidth);
canvas.drawCircle(x, y, radius - borderWidth / 2.0f, paint);
}
到此,我们的CircleImageView就写完了,还有一些取值,设置函数,用于在代码动态添加圆形图片,大家自己去添加。
接下来,我们就看看使用的效果:
CircleImageView使用
布局文件代码如下:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:zcw="http://schemas.android.com/apk/res/com.zcw.circleimageview"
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray" >
<LinearLayout
android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="View" />
<com.zcw.circleimageview.view.CircleImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="5dp"
android:src="@drawable/pic3" />
<com.zcw.circleimageview.view.CircleImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="5dp"
android:src="@drawable/pic4"
zcw:border_width="5dp"
zcw:border_corlor="@android:color/holo_purple" />
<com.zcw.circleimageview.view.CircleImageView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_marginTop="5dp"
android:padding="10dp"
android:src="@drawable/pic3"
zcw:border_width="5dp"
zcw:border_corlor="@android:color/holo_purple"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
</ScrollView>
eclipse布局文件中使用自定义属性,需要在布局文件的根控件下添加以下语句(com.zcw.circleimageview为包名):
xmlns:zcw="http://schemas.android.com/apk/res/com.zcw.circleimageview"
Android studio中,使用自定义属性,需要添加的语句,和eclipse中略有不同,没有了包名。
xmlns:app="http://schemas.android.com/apk/res-auto"
运行出来的效果图,就是第二张图的样子了。
下一遍文章,将会分析圆形图片ShaderBitmap实现方式的代表作,Henning Dodenhof的开源项目https://github.com/hdodenhof/CircleImageView。