淺談Android實(shí)現(xiàn)圓形頭像效果的幾種思路和方法

前言

我們在開發(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):視覺和交互。視覺由onMeasureonLayout、onDraw這三個方法來完成,而交互則是由dispatchTouchEvent、onInterceptTouchEventonTouchEvent等這幾個方法來控制,只要處理好這幾個方法,我們就能實(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)的效果是一樣的,都是下面所示:


圓形頭像實(shí)現(xiàn)效果1

思路二:從自定義Drawable出發(fā)

我們知道,Drawable可以作為ImageView的一個圖片來源。Drawable,從名字上來看,就是可繪制的意思,實(shí)際上它內(nèi)部有一個draw(canvas)方法,我們拿到的canvas對象之后,就能在畫布上繪制我們想要的東西了,其實(shí)核心原理跟上面的差不多,都是通過canvaspaint對一個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í)也是利用CanvasPaint,重新繪制一個圖片,再加上別的效果,通過這種形式就實(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);

下面,我們來看看上述三個思路所得到的效果是怎樣的:


圓形頭像2

可以看出,三個效果都還不錯。

總結(jié)

當(dāng)我們需要對一個Bitmap進(jìn)行處理,實(shí)現(xiàn)不同的效果的時候,首先應(yīng)該想到利用CanvasPaint來處理,然后結(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é)束,希望對你有所幫助~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容