CircleImageView
A fast circular ImageView perfect for profile images. This is based on RoundedImageView from Vince Mi which itself is based on techniques recommended by Romain Guy.

It uses a BitmapShader and does not:
- create a copy of the original bitmap
- use a clipPath (which is neither hardware accelerated nor anti-aliased)
- use setXfermode to clip the bitmap (which means drawing twice to the canvas)
As this is just a custom ImageView and not a custom Drawable or a combination of both, it can be used with all kinds of drawables, i.e. a PicassoDrawable from Picasso or other non-standard drawables (needs some testing though).
Gradle
dependencies {
...
compile 'de.hdodenhof:circleimageview:2.1.0'
}
Usage
<de.hdodenhof.circleimageview.CircleImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/profile_image"
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@drawable/profile"
app:civ_border_width="2dp"
app:civ_border_color="#FF000000"/>
Demo
/**
* 流程控制的比較嚴(yán)謹(jǐn)鞍帝,比如setup函數(shù)的使用
* updateShaderMatrix保證圖片損失度最小和始終繪制圖片正中央的那部分
* 作者思路是畫圓用渲染器位圖填充沐旨,而不是把Bitmap重繪切割成一個圓形圖片闷串。
*/
public class CircleImageView extends android.support.v7.widget.AppCompatImageView {
//縮放類型
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int COLORDRAWABLE_DIMENSION = 2;
// 默認(rèn)邊界寬度
private static final int DEFAULT_BORDER_WIDTH = 0;
// 默認(rèn)邊界顏色
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
private static final boolean DEFAULT_BORDER_OVERLAY = false;
//覆蓋矩形
private final RectF mDrawableRect = new RectF();
//邊界矩形
private final RectF mBorderRect = new RectF();
//位圖變換
private final Matrix mShaderMatrix = new Matrix();
//這個畫筆最重要的是關(guān)聯(lián)了mBitmapShader
// 使canvas在執(zhí)行的時候可以切割原圖片(mBitmapShader是關(guān)聯(lián)了原圖的bitmap的)
private final Paint mBitmapPaint = new Paint();
//這個描邊,則與本身的原圖bitmap沒有任何關(guān)聯(lián)畏纲,
private final Paint mBorderPaint = new Paint();
//這里定義了 圓形邊緣的默認(rèn)寬度和顏色
private int mBorderColor = DEFAULT_BORDER_COLOR;
private int mBorderWidth = DEFAULT_BORDER_WIDTH;
private Bitmap mBitmap;
private BitmapShader mBitmapShader; // 位圖渲染
private int mBitmapWidth; // 位圖寬度
private int mBitmapHeight; // 位圖高度
private float mDrawableRadius;// 圖片半徑
private float mBorderRadius;// 帶邊框的的圖片半徑
private ColorFilter mColorFilter;
//初始false
private boolean mReady;
private boolean mSetupPending;
private boolean mBorderOverlay;
//構(gòu)造函數(shù)
public CircleImageView(Context context) {
super(context);
init();
}
//構(gòu)造函數(shù)
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init();
}
/**
* 構(gòu)造函數(shù)
*/
public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//通過obtainStyledAttributes 獲得一組值賦給 TypedArray(數(shù)組) , 這一組值來自于res/values/attrs.xml中的name="CircleImageView"的declare-styleable中。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
//通過TypedArray提供的一系列方法getXXXX取得我們在xml里定義的參數(shù)值裹赴;
// 獲取邊界的寬度
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView2_border_width, DEFAULT_BORDER_WIDTH);
// 獲取邊界的顏色
mBorderColor = a.getColor(R.styleable.CircleImageView2_border_color, DEFAULT_BORDER_COLOR);
mBorderOverlay = a.getBoolean(R.styleable.CircleImageView2_border_overlay, DEFAULT_BORDER_OVERLAY);
//調(diào)用 recycle() 回收TypedArray
a.recycle();
System.out.println("CircleImageView -- 構(gòu)造函數(shù)");
init();
}
/**
* 作用就是保證第一次執(zhí)行setup函數(shù)里下面代碼要在構(gòu)造函數(shù)執(zhí)行完畢時調(diào)用
*/
private void init() {
//在這里ScaleType被強(qiáng)制設(shè)定為CENTER_CROP羡忘,就是將圖片水平垂直居中,進(jìn)行縮放案狠。
super.setScaleType(SCALE_TYPE);
//準(zhǔn)備完畢
mReady = true;
//是否開始設(shè)置
if (mSetupPending) {
setup();
mSetupPending = false;
}
}
/**
* 返回縮放類型
* @return
*/
@Override
public ScaleType getScaleType() {
return SCALE_TYPE;
}
/**
* 這里明確指出 此種imageview 只支持CENTER_CROP 這一種屬性
*
* @param scaleType
*/
@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType != SCALE_TYPE) {
throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
}
}
/**
* 明確指出不支持調(diào)整邊界
* @param adjustViewBounds
*/
@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (adjustViewBounds) {
throw new IllegalArgumentException("adjustViewBounds not supported.");
}
}
/**
* 最定義關(guān)鍵
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//如果圖片不存在就不畫
if (getDrawable() == null) {
return;
}
//繪制內(nèi)圓形 畫筆為mBitmapPaint
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint);
//如果圓形邊緣的寬度不為0 我們還要繪制帶邊界的外圓形 邊界畫筆為mBorderPaint
if (mBorderWidth != 0) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
setup();
}
public int getBorderColor() {
return mBorderColor;
}
public void setBorderColor(int borderColor) {
if (borderColor == mBorderColor) {
return;
}
mBorderColor = borderColor;
mBorderPaint.setColor(mBorderColor);
invalidate();
}
public void setBorderColorResource(@ColorRes int borderColorRes) {
setBorderColor(getContext().getResources().getColor(borderColorRes));
}
public int getBorderWidth() {
return mBorderWidth;
}
public void setBorderWidth(int borderWidth) {
if (borderWidth == mBorderWidth) {
return;
}
mBorderWidth = borderWidth;
setup();
}
public boolean isBorderOverlay() {
return mBorderOverlay;
}
public void setBorderOverlay(boolean borderOverlay) {
if (borderOverlay == mBorderOverlay) {
return;
}
mBorderOverlay = borderOverlay;
setup();
}
/**
* 以下四個函數(shù)都是
* 復(fù)寫ImageView的setImageXxx()方法
* 注意這個函數(shù)先于構(gòu)造函數(shù)調(diào)用之前調(diào)用
* @param bm
*/
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
mBitmap = bm;
setup();
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
mBitmap = getBitmapFromDrawable(drawable);
System.out.println("setImageDrawable -- setup");
setup();
}
@Override
public void setImageResource(@DrawableRes int resId) {
super.setImageResource(resId);
mBitmap = getBitmapFromDrawable(getDrawable());
setup();
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
mBitmap = getBitmapFromDrawable(getDrawable());
setup();
}
@Override
public void setColorFilter(ColorFilter cf) {
if (cf == mColorFilter) {
return;
}
mColorFilter = cf;
mBitmapPaint.setColorFilter(mColorFilter);
invalidate();
}
/**
* Drawable轉(zhuǎn)Bitmap
* @param drawable
* @return
*/
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
//通常來說 我們的代碼就是執(zhí)行到這里就返回了服傍。返回的就是我們最原始的bitmap
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION,
COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), BITMAP_CONFIG);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (OutOfMemoryError e) {
return null;
}
}
/**
* 這個函數(shù)很關(guān)鍵,進(jìn)行圖片畫筆邊界畫筆(Paint)一些重繪參數(shù)初始化:
* 構(gòu)建渲染器BitmapShader用Bitmap來填充繪制區(qū)域,設(shè)置樣式以及內(nèi)外圓半徑計算等骂铁,
* 以及調(diào)用updateShaderMatrix()函數(shù)和 invalidate()函數(shù)吹零;
*/
private void setup() {
//因為mReady默認(rèn)值為false,所以第一次進(jìn)這個函數(shù)的時候if語句為真進(jìn)入括號體內(nèi)
//設(shè)置mSetupPending為true然后直接返回,后面的代碼并沒有執(zhí)行拉庵。
if (!mReady) {
mSetupPending = true;
return;
}
//防止空指針異常
if (mBitmap == null) {
return;
}
// 構(gòu)建渲染器灿椅,用mBitmap位圖來填充繪制區(qū)域 ,參數(shù)值代表如果圖片太小的話 邊緣處拉伸
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 設(shè)置圖片畫筆反鋸齒
mBitmapPaint.setAntiAlias(true);
// 設(shè)置圖片畫筆渲染器
mBitmapPaint.setShader(mBitmapShader);
// 設(shè)置邊界畫筆樣式
mBorderPaint.setStyle(Paint.Style.STROKE);//設(shè)畫筆為空心
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor); //畫筆顏色
mBorderPaint.setStrokeWidth(mBorderWidth);//畫筆邊界寬度
//這個地方是取的原圖片的寬高
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
// 設(shè)置含邊界顯示區(qū)域,取的是CircleImageView的布局實際大小茫蛹,為方形操刀,查看xml也就是160dp(240px)
// getWidth得到是某個view的實際尺寸
mBorderRect.set(0, 0, getWidth(), getHeight());
//計算 圓形帶邊界部分(外圓)的最小半徑,
// 取mBorderRect的寬高減去一個邊緣大小的一半的較小值(這個地方我比較納悶為什么求外圓半徑需要先減去一個邊緣大杏ね荨)
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2,
(mBorderRect.width() - mBorderWidth) / 2);
// 初始圖片顯示區(qū)域為mBorderRect(CircleImageView的布局實際大泄强印)
mDrawableRect.set(mBorderRect);
if (!mBorderOverlay) {
//demo里始終執(zhí)行
//通過inset方法 使得圖片顯示的區(qū)域從mBorderRect大小上下左右內(nèi)移邊界的寬度形成區(qū)域
// ,查看xml邊界寬度為2dp(3px),所以方形邊長為就是160-4=156dp(234px)
mDrawableRect.inset(mBorderWidth, mBorderWidth);
}
//這里計算的是內(nèi)圓的最小半徑窃蹋,也即去除邊界寬度的半徑
mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2);
//設(shè)置渲染器的變換矩陣也即是mBitmap用何種縮放形式填充
updateShaderMatrix();
//手動觸發(fā)ondraw()函數(shù) 完成最終的繪制
invalidate();
}
/**
* 這個函數(shù)為設(shè)置BitmapShader的Matrix參數(shù)卡啰,設(shè)置最小縮放比例,平移參數(shù)警没。
* 作用:保證圖片損失度最小和始終繪制圖片正中央的那部分
*/
private void updateShaderMatrix() {
float scale;
float dx = 0;
float dy = 0;
mShaderMatrix.set(null);
//取最小的縮放比例
/* bW bH
---- = -----
mDW mDH
*/
if (mBitmapWidth * mDrawableRect.height() > mBitmapHeight*mDrawableRect.width() ) {
//y軸縮放 x軸平移 使得圖片的y軸方向的邊的尺寸縮放到圖片顯示區(qū)域(mDrawableRect)一樣)
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
} else {
//x軸縮放 y軸平移 使得圖片的x軸方向的邊的尺寸縮放到圖片顯示區(qū)域(mDrawableRect)一樣)
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
}
// shaeder的變換矩陣匈辱,我們這里主要用于放大或者縮小。
mShaderMatrix.setScale(scale, scale);
// 平移
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
// 設(shè)置變換矩陣
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
}
Limitations
- The ScaleType is always CENTER_CROP and you'll get an exception if you try to change it. This is (currently) by design as it's perfectly fine for profile images.
- Enabling
adjustViewBounds
is not supported as this requires an unsupported ScaleType - If you use an image loading library like Picasso or Glide, you need to disable their fade animations to avoid messed up images. For Picasso use the
noFade()
option, for Glide usedontAnimate()
. If you want to keep the fadeIn animation, you have to fetch the image into aTarget
and apply a custom animation yourself when receiving theBitmap
. - Using a
TransitionDrawable
withCircleImageView
doesn't work properly and leads to messed up images.
FAQ
How can I use a VectorDrawable
with CircleImageView
?
Short answer: you shouldn't. Using a VectorDrawable
with CircleImageView
is very inefficient. You should modify your vectors to be in a circular shape and use them with a regular ImageView instead.
Why doesn't CircleImageView
extend AppCompatImageView
?
Extending AppCompatImageView
would require adding a runtime dependency for the support library without any real benefit.
How can I add a selector (e.g. ripple effect) bound to a circle?
There's currently no direct support for a circle bound selector but you can follow these steps to implement it yourself.
How can I add a gap between image and border?
Adding a gap is also not support directly but there's a workaround.
Changelog
-
next
- Add support for elevation
- Add circle background color attribute to replace fill color
-
2.1.0
- Add support for padding
- Add option to disable circular transformation
- Fix hairline gap being drawn between image and border under some conditions
- Fix NPE when using tint attribute (which is not supported)
- Deprecate fill color as it seems to cause quite some confusion
-
2.0.0
- BREAKING: Custom xml attributes are now prefixed with "civ_"
- Graceful handling of incompatible drawables
- Add support for a fill color shown behind images with transparent areas
- Fix dimension calculation issues with small images
- Fix bitmap not being cleared when set to null
-
1.3.0
- Add setBorderColorResource(int resId)
- Add resource type annotations
- Add border_overlay attribute to allow drawing border on top of the base image
-
1.2.2
- Add ColorFilter support
-
1.2.1
- Fix ColorDrawables not being rendered properly on Lollipop
-
1.2.0
- Add support for setImageURI(Uri uri)
- Fix view not being initialized when using CircleImageView(Context context)
-
1.1.1
- Fix border being shown although border width is set to 0
-
1.1.0
- Add support for ColorDrawables
- Add getters and setters for border color and border width
-
1.0.1
- Prevent crash due to OutOfMemoryError
-
1.0.0
- Initial release