前言
我們在開發(fā)的過程中,經(jīng)常會遇到如下的需求:在界面上展示圓形的用戶頭像,其實(shí)這個需求很普遍并且實(shí)現(xiàn)難度也不大,網(wǎng)上也有很多相關(guān)的教程,那么本文主要來對幾種實(shí)現(xiàn)思路和方法進(jìn)行一次總結(jié),方便以后需要時可以隨時查閱。
兩個核心方法
對于圓形頭像的實(shí)現(xiàn),實(shí)際上就是對長方形頭像的Bitmap作某些處理,以達(dá)到變換成圓形頭像的效果。我們自然而然地想到了用Canvas和Paint來處理,利用它們我們能實(shí)現(xiàn)很多視覺上的效果。因?yàn)镃anvas實(shí)際上就是一種畫布,我們能在上面畫出Bitmap再配合其他技術(shù)來實(shí)現(xiàn)我們的需求。在這里,主要用到了以下兩個類:
1、PorterDuffXfermode 圖層混合技術(shù),一個Canvas上有多個圖層,在不同的圖層上繪制不同的圖案,然后把不同的圖層繪制到同一張Canvas后,由于采用了某個規(guī)則來進(jìn)行圖層混合,所以可以得到不同的最終繪制效果。
2、BitmapShader圖像渲染器,它是Shader渲染器的一個子類,可以在繪制某一圖像的時候,把另一圖像同時渲染上去。比如我們繪制一個圓形,同時把方形頭像渲染上去,便實(shí)現(xiàn)了圓形頭像。
以上兩個類都可以通過畫筆Paint的mPaint.setXfermode()和mPaint.setShader()來調(diào)用,只要使用其中一種便能實(shí)現(xiàn)圓形頭像的效果。
下面,我們利用上述兩個方法來實(shí)現(xiàn)一下圓形頭像。
思路一:從View的角度出發(fā)
首先,拿到一個視覺方面的需求時,我們可以考慮是否能從自定義View的角度出發(fā)來解決問題,一般通過自定義View都能解決很多視覺方面的需求。簡單來說,自定義View的核心無非在于兩點(diǎn):視覺和交互。視覺由onMeasure、onLayout、onDraw這三個方法來完成,而交互則是由dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent等這幾個方法來控制,只要處理好這幾個方法,我們就能實(shí)現(xiàn)形態(tài)各異的自定義View了。
那么,回到本文所討論的問題:實(shí)現(xiàn)一個圓形頭像需要做些什么呢?
首先,我們需要在onMeasure方法里面對View的寬高做出調(diào)整,因?yàn)橐话銏A形頭像的寬高都是相等的。
其次,我們需要在onDraw方法內(nèi)實(shí)現(xiàn)圓形頭像的邏輯。而實(shí)現(xiàn)圓形頭像的邏輯,我們首先需要拿到原始頭像的圖片,我們這里可以直接繼承ImageView,以方便我們直接拿到設(shè)置到ImageView的圖片。拿到Bitmap之后,我們就可以對它進(jìn)行處理了,我們先來看看代碼是怎樣實(shí)現(xiàn)的。
(1)利用PorterDuffXfermode技術(shù)來實(shí)現(xiàn)
自定義一個CircleImageView繼承自AppCompatImageView:
public class CircleImageView extends AppCompatImageView {
private int mSize;
private Paint mPaint;
private Xfermode mPorterDuffXfermode;
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);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setDither(true);
mPaint.setAntiAlias(true);
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
mSize = Math.min(width,height); //取寬高的最小值
setMeasuredDimension(mSize,mSize); //設(shè)置CircleImageView為等寬高
}
@Override
protected void onDraw(Canvas canvas) {
//獲取sourceBitmap,即通過xml或者java設(shè)置進(jìn)來的圖片
Drawable drawable = getDrawable();
if (drawable == null) return;
Bitmap sourceBitmap = ((BitmapDrawable)getDrawable()).getBitmap();
if (sourceBitmap != null){
//對圖片進(jìn)行縮放,以適應(yīng)控件的大小
Bitmap bitmap = resizeBitmap(sourceBitmap,getWidth(),getHeight());
drawCircleBitmapByXfermode(canvas,bitmap); //(1)利用PorterDuffXfermode實(shí)現(xiàn)
//drawCircleBitmapByShader(canvas,bitmap); //(2)利用BitmapShader實(shí)現(xiàn)
}
}
private Bitmap resizeBitmap(Bitmap sourceBitmap,int dstWidth,int dstHeight){
int width = sourceBitmap.getWidth();
int height = sourceBitmap.getHeight();
float widthScale = ((float)dstWidth) / width;
float heightScale = ((float)dstHeight) / height;
//取最大縮放比
float scale = Math.max(widthScale,heightScale);
Matrix matrix = new Matrix();
matrix.postScale(scale,scale);
return Bitmap.createBitmap(sourceBitmap,0,0,width,height,matrix,true);
}
private void drawCircleBitmapByXfermode(Canvas canvas,Bitmap bitmap){
final int sc = canvas.saveLayer(0,0,getWidth(),getHeight(),null,Canvas.ALL_SAVE_FLAG);
//繪制dst層
canvas.drawCircle(mSize / 2,mSize / 2,mSize / 2,mPaint);
//設(shè)置圖層混合模式為SRC_IN
mPaint.setXfermode(mPorterDuffXfermode);
//繪制src層
canvas.drawBitmap(bitmap,0,0,mPaint);
canvas.restoreToCount(sc);
}
}
現(xiàn)在分析一下上面代碼實(shí)現(xiàn)的邏輯:
①當(dāng)我們拿到一個Bitmap時,首先要對它進(jìn)行縮放,因?yàn)樗蟾怕噬隙际遣贿m合我們當(dāng)前控件的寬高的。上面的resize()方法,這里的縮放利用了Matrix對原始Bitmap進(jìn)行了等比列縮放生成了一個新的Bitmap,新的Bitmap與控件的寬或者高是相等的,也即是原始Bitmap與控件大小差不多了(至少寬或者高相等)。
②接著,我們來看drawCircleBitmapByXfermode()方法,這里利用了PorterDuffxfermode技術(shù)來實(shí)現(xiàn)。首先調(diào)用canvas.saveLayer方法生成了一個透明的Bitmap,然后繪制dst圖層在下面,接著把圖層混合模式設(shè)置成了SRC_IN模式,表示SRC和DST圖層混合后,顯示交集的SRC部分。然后繪制SRC層,這樣就實(shí)現(xiàn)了圓形頭像。因?yàn)閳A形和方形頭像的圖層混合后,交集部分就是圓形頭像的部分了。
(2)利用BitmapShader實(shí)現(xiàn)
我們在上面代碼的末尾添加多一個方法即可:
private void drawCircleBitmapByShader(Canvas canvas,Bitmap bitmap){
BitmapShader shader = new BitmapShader(bitmap,BitmapShader.TileMode.CLAMP,BitmapShader.TileMode.CLAMP);
mPaint.setShader(shader);
canvas.drawCircle(mSize / 2,mSize /2 ,mSize / 2,mPaint);
}
上面的代碼很簡單,就是生成一個BitmapShader,然后綁定到畫筆Paint上,這樣在繪制圓形的同時也會在圓形上渲染出Bitmap了。
我們運(yùn)行程序,會發(fā)現(xiàn)兩種方法實(shí)現(xiàn)的效果是一樣的,都是下面所示:

思路二:從自定義Drawable出發(fā)
我們知道,Drawable可以作為ImageView的一個圖片來源。Drawable,從名字上來看,就是可繪制的意思,實(shí)際上它內(nèi)部有一個draw(canvas)方法,我們拿到的canvas對象之后,就能在畫布上繪制我們想要的東西了,其實(shí)核心原理跟上面的差不多,都是通過canvas與paint對一個Bitmap進(jìn)行繪制的同時,通過渲染器或者通過圖層混合技術(shù)來實(shí)現(xiàn)圓形頭像的效果。
我們新建一個CircleImageDrawable繼承自Drawable,代碼如下:
public class CircleImageDrawable extends Drawable {
private Bitmap mBitmap;
private Paint mPaint;
private BitmapShader mBitmapShader;
private int mSize;
private int mRadius;
private static final String TAG = "CircleImageDrawable";
public CircleImageDrawable(Bitmap bitmap) {
this.mBitmap = bitmap;
mBitmapShader = new BitmapShader(bitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setShader(mBitmapShader);
mSize = Math.min(bitmap.getWidth(),bitmap.getHeight());
mRadius = mSize / 2;
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.drawCircle(mRadius,mRadius,mRadius,mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
}
在構(gòu)造方法內(nèi),我們就對Paint設(shè)置了BitmapShader,跟思路一的代碼如出一轍,而使用PorterDuffXfermode的方式則是在draw(canvas)內(nèi)完成,這里就不再贅述。
思路三:使用Picasso的Transformation來實(shí)現(xiàn)
一般我們加載圖片的時候,都會使用一個圖片加載框架來幫助我們處理加載的異步過程、以及緩存機(jī)制等,比如Picass和Glide等都支持很多功能。其實(shí)我們在加載的時候,還能添加一個轉(zhuǎn)換器Transformation,對圖片進(jìn)行變換,來實(shí)現(xiàn)圓角、高斯模糊等豐富多彩的效果。其核心原理其實(shí)也是利用Canvas和Paint,重新繪制一個圖片,再加上別的效果,通過這種形式就實(shí)現(xiàn)了圖形的變換,而實(shí)現(xiàn)圓角圖片也就很簡單了,我們來實(shí)踐一下吧!
新建一個CircleImageTransformer繼承自Transformation,代碼如下:
public class CircleImageTransformer implements Transformation {
@Override
public Bitmap transform(Bitmap source) {
//去圖片寬高的最小值,變成正方形圖片
int size = Math.min(source.getWidth(),source.getHeight());
Bitmap bitmap = Bitmap.createBitmap(size,size,source.getConfig());
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
BitmapShader shader = new BitmapShader(source,BitmapShader.TileMode.CLAMP,BitmapShader.TileMode.CLAMP);
paint.setShader(shader);
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r,r,r,paint);
source.recycle();
return bitmap;
}
@Override
public String key() {
return "Circle-Image";
}
}
而使用的時候,可以這樣:
Picasso.with(this)
.load(R.drawable.ic_captain_america)
.transform(new CircleImageTransformer())
.into(imageView3);
下面,我們來看看上述三個思路所得到的效果是怎樣的:

可以看出,三個效果都還不錯。
總結(jié)
當(dāng)我們需要對一個Bitmap進(jìn)行處理,實(shí)現(xiàn)不同的效果的時候,首先應(yīng)該想到利用Canvas和Paint來處理,然后結(jié)合Android官方提供的各種庫來實(shí)現(xiàn)需要的效果。上面三個思路分別從自定義View、自定義Drawable和自定義轉(zhuǎn)換器三個角度來實(shí)現(xiàn)圓形頭像效果,其核心都是一樣的,都是利用了BitmapShader或者PorterDuffXfermode來實(shí)現(xiàn)。本文的主要目的是拋磚引玉,給出一個大體的思路和實(shí)現(xiàn)方法,但實(shí)際上還是有很多可以優(yōu)化的地方,比如圖片壓縮裁切、臉部檢測等,要完整實(shí)現(xiàn)一個圓形頭像也還是有不少細(xì)節(jié)需要注意的,但具體問題具體分析,思路有了,解決問題的方案也就在不遠(yuǎn)處了。好了,本文到此結(jié)束,希望對你有所幫助~