看标题就知道,这是一个系列,因为实现圆形ImageView的方法有很多种,所以接下来准备将这几种方法都实现一遍,最后总结对比下各种方法。
虽然网上已经有很多现成的分析文章以及源代码,但是毕竟是别人总结的,还是没有自己实践来的真实。
纸上得来终觉浅,绝知此事要躬行哈。
下面就开始来说最简单常用的一种方法:自定义View+设置Xfermode的方式。先给大家看效果图:
关于Xfermode相关的知识,这里不多做介绍,只稍微提一下。会用ps的同学应该都用过选框工具,选框工具也有不同的模式,分别是:
- 新选区
- 添加到选区
- 从选区减去
- 与选区交叉
总之就是各种取交集、并集的过程。Xfermode的各种不同模式思想上跟这个有点类似,不过比ps的选区强大得多,大家看图就明白了:
可以看到Xfermode的取值有很多种,不同的取值可以达到不同的效果,类似于一些刮刮卡以及撕美女裙子等的游戏应该都可以利用设置Xfermode的不同值来实现。
对于上图中的不同取值,网上有人总结出来了下面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
有的同学对于上面的src和dest可能分不清楚,究竟先画的是src还是dest呢?分析google给的android apidemo里面graphics/xfermode里面的onDraw方法可以看到
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG);
labelP.setTextAlign(Paint.Align.CENTER);
Paint paint = new Paint();
paint.setFilterBitmap(false);
canvas.translate(15, 35);
int x = 0;
int y = 0;
for (int i = 0; i < sModes.length; i++) {
// draw the border
paint.setStyle(Paint.Style.STROKE);
paint.setShader(null);
canvas.drawRect(x - 0.5f, y - 0.5f,
x + W + 0.5f, y + H + 0.5f, paint);
// draw the checker-board pattern
paint.setStyle(Paint.Style.FILL);
paint.setShader(mBG);
canvas.drawRect(x, y, x + W, y + H, paint);
// draw the src/dst example into our offscreen bitmap
int sc = canvas.saveLayer(x, y, x + W, y + H, null,
Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
canvas.translate(x, y);
canvas.drawBitmap(mDstB, 0, 0, paint);
paint.setXfermode(sModes[i]);
canvas.drawBitmap(mSrcB, 0, 0, paint);
paint.setXfermode(null);
canvas.restoreToCount(sc);
// draw the label
canvas.drawText(sLabels[i],
x + W/2, y - labelP.getTextSize()/2, labelP);
x += W + 10;
// wrap around when we've drawn enough for one row
if ((i % ROW_MAX) == ROW_MAX - 1) {
x = 0;
y += H + 30;
}
}
}
这段代码完成的效果也就是上面那个xfermode效果对照表所展示的效果了,可以看到onDraw里面的第35~38行中,可以非常清楚的知道:
先画mDstB
然后设置xfermode
最后画mSrc
也就是先画的图是作为dest画在canvas 的下层的,后画的图是作为src画在canvas的上层的。
对于我们这个圆形的ImageView的话,主要是使用srcIn的模式即可:在canvas下层画一个圆,然后将图片画在圆上,取两者的交集并且显示上层,就可以达到圆形图片的效果了。
方法如下:
- 在canvas上画一个圆;
- 调用paint.setXfermode设置为srcIn模式;
- 将要显示的图作为Src画在上面的那个圆上;
- 两次绘图之后叠加起来的效果就是圆形的图片了;
那么能不能先画图片然后画圆,将xfermode设置成destIn呢?试了一下发现不行,这是为什么呢?(用官方的demo调换绘制顺序并且改成destin是可以实现srcin的效果的)
主要原理已经介绍完了,当然除了设置Xfermode的值,还需要考虑其他一些小细节,比如图片和圆的相对位置、图片和view的相对大小等,纯粹就是计算了,也很简单。
先贴代码吧。
先在attr.xml中声明两个自定义属性,以支持设置默认图片和初始图片,比如从网络获取一张图片时你可能需要一张默认图片显示,就可以设置默认图片了,代码如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="image" format="reference" />
<attr name="defaultImage" format="reference" />
<declare-styleable name="CircleImageView">
<attr name="image" />
<attr name="defaultImage" />
</declare-styleable>
</resources>
然后在布局文件中声明xml命名空间,如下
xmlns:circleImage="http://schemas.android.com/apk/res-auto"
接下来就可以使用自定义属性了
<com.passerby.androidadvanced.circleimage.xfermode.CircleImageView
android:layout_width="200dp"
android:layout_height="200dp"
circleImage:defaultImage="@drawable/image"/>
CircleImageView源码如下:
package com.passerby.androidadvanced.circleimage.xfermode;
import com.passerby.circleimage.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
/** * Created by mac on 16/2/1. */
public class CircleImageView extends View {
private Paint mPaint;
private Bitmap mSrcBitmap;
private Bitmap mDefaultBitmap;
public CircleImageView(Context context) {
this(context, null);
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyleAttr, 0);
int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int resId;
int index = a.getIndex(i);
switch (index) {
case R.styleable.CircleImageView_defaultImage:
resId = a.getResourceId(index, 0);
mDefaultBitmap = BitmapFactory.decodeResource(getResources(), resId);
break;
case R.styleable.CircleImageView_image:
resId = a.getResourceId(index, 0);
mSrcBitmap = BitmapFactory.decodeResource(getResources(), resId);
break;
}
}
a.recycle();
if (null == mSrcBitmap) {
mSrcBitmap = mDefaultBitmap;
}
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
//取宽、高中的较小值
int min = width > height ? height : width;
if (null != mSrcBitmap) {
canvas.drawBitmap(createCircleBitmap(mSrcBitmap, min), 0, 0, mPaint);
}
}
/*** * 将传入的bitmap转换成圆的正方形图片 * * @param bitmap * @param min 目标bitmap的高 * @return */
private Bitmap createCircleBitmap(Bitmap bitmap, int min) {
Paint paint = new Paint();
paint.setAntiAlias(true);
final int tWidth = getMeasuredWidth();
final int tHeight = getMeasuredHeight();
//创建一个新的bitmap,大小跟imageview一样
Bitmap target = Bitmap.createBitmap(tWidth, tHeight, Bitmap.Config.ARGB_8888);
//下面所有的绘图操作均在这个target上面完成
Canvas canvas = new Canvas(target);
//1.画圆形
//计算圆的圆心在imageview中的坐标
int halfWidth = tWidth >> 1;
int halfHeight = tHeight >> 1;
//得到圆的半径
int radius = min;
//画圆
canvas.drawCircle(halfWidth, halfHeight, radius >> 1, paint);
//2.设置Xfermode
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//3.画图片
//将图片画在imageview居中的位置
Rect srcRect = new Rect(0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight());
int offsetX = (tWidth - min) >> 1;
int offsetY = (tHeight - min) >> 1;
Rect dstRect = new Rect(offsetX, offsetY, offsetX + min, offsetY + min);
canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
return target;
}
public void setImage(int resId) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
setImage(bitmap);
}
public void setImage(Bitmap bitmap) {
mSrcBitmap = bitmap;
invalidate();
}
}
代码第37~53行是获取属性,得到用户指定的默认图片和直接显示的图片。
55~58行,如果用户没有指定直接显示的图片,就使用默认图片。
67~70行,获取控件宽和高中的较小值min,因为要显示圆形图片,所以要以min作为那个圆的直径。
71~74行,画圆形图片。createCircleBitmap()方法是核心,主要注释都写好了,就不多提了。
对于上面代码实现的思路,是直接继承View然后将图片弄成圆形画出来,这种方法适合只需要圆形图片的需求,像用户头像之类的。
当然也可以继承ImageView然后在上面盖一个遮罩层,也可以实现圆形ImageView的效果,而且可以同时保留ImageView相关的很多属性。
第二种方法请看这里《 圆形ImageView系列(二)—–Xfermode+ImageView》。