Android之圓形頭像裁切

weixin_34238633發表於2016-08-24

PS:今天專案測試組發現,百度地圖定位的資料座標位置是正確的,但是顯示的資料是錯誤的.最後查來查去發現,那個商廈在百度地圖上根本就沒有那條資料,這讓我如何顯示,當初就推崇使用高德地圖定位,上面的資料量比較的完整,而且定位的也比較的精準,非得用百度地圖定位,這下定位不到資料,懵逼了吧..

 

學習內容:

1.自定義View+Canvas+XferMode實現圓形頭像裁切

 

  頭像裁切現如今在很多應用中都會得到使用,一般都是在個人資料頁面設定頭像,然後選擇圖片,或者是直接開啟相機拍攝一張圖片,通過裁切和縮放的手段最後顯示在ImageView上就可以了.不過無論怎樣裁切其實最後裁切出來仍然是一個方形的圖片,因此我們需要自定義ImageView,將ImageView定義成我們想要的形狀,然後將裁切到的圖片顯示到ImageView上就可以了.這裡的ImageView我是使用的第三方框架,因為自己還沒有打算自定義ImageView這塊.因此就直接用了別人的東西.

 

i.Canvas的saveLayer(),restore()方法.

  實現頭像裁切需要使用幾種技術,首先就需要Canvas的支援,首先說一下他的結構組成,這樣更加方便理解.

 

  Canvas的結構基本是這樣的,在View繪製到螢幕上的時候在OnDraw()方法在呼叫的時候,所有的控制元件就會通過Paint繪製到Canvas上.其實就是畫到畫布上,預設情況下,我們可以把Canvas也看作成一個Layer,當我們在saveLayer()的時候,就表示我們開啟一個新的圖層,所有繪製的內容都會在當前圖層完成,不會影響到前一張圖層,相當於圖層的覆蓋,當我們呼叫restore()方法的時候,那麼當前圖層出棧,將所有的內容繪製到被覆蓋的圖層.簡單的說說saveLayer()方法如何使用.

 這裡我們在Canvas上先畫了一個紅色的圓圈,然後又入棧一個帶有透明度的Layer,在當前這個Layer畫一個藍色的圓圈.

package com.example.totem.canvastest.activity;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;


public class LayersActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SimpleView(this));
    }

    private static class SimpleView extends View {
        private static final int LAYER_FLAGS = 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;

        private Paint mPaint = new Paint();

        public SimpleView(Context context) {
            super(context);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);
            canvas.translate(10, 10);
            mPaint.setColor(Color.RED);
            canvas.drawCircle(75, 75, 75, mPaint);
            /**
             * 入棧一個Layer,當前現在有兩個Layer,因為Canvas其實也可以看成是一個Layer
* 這裡入棧了一個帶有透明度的Layer *
*/ canvas.saveLayerAlpha(0, 0, 200, 200, 0x88, LAYER_FLAGS); mPaint.setColor(Color.BLUE); canvas.drawCircle(125, 125, 75, mPaint); canvas.restore(); } } } 

  然後我們呼叫restore()方法,那麼畫在透明層的藍色圓圈就需要被重新繪製到當前的Canvas層上,因此可以看到,紅色圓圈和藍色圓圈疊加的狀態,並且中間是有透明度的.如果比入棧一個新的圖層,會出現明顯的效果差異.大家可以將這句話註釋掉執行一下看看效果.

 因為在頭像裁切的時候我們需要使用多層畫布結合XferMode實現複雜圖形,因此先在這裡簡單的介紹一下,以免在後續看到這塊程式碼不知道具體是要做什麼用的.

ii.Xfermode

 Xfermode也是實現這個效果的一個核心,它是實現圖形混合的一種模式,由Tomas Proter和 Tom Duff提出的概念,Xfermode只是一個基類,具有三個子類,分別是AvoidXfermode,PixelorXfermode,PorterDuffXfermode.不過前兩個在api16以後就已經棄用了,因此前面這兩個我也沒打算說,主要還是說一下PorterDuffXfermode這種模式.

 PorterDuffXfermode一共有18種圖形混排模式.那麼就來介紹一下這18種模式,以及這18中模式的出現所導致的效果.

PorterDuff.Mode
 模式+說明 計算方式
PorterDuff.Mode
ADD(飽和相加) Saturate(S + D)

  PorterDuff.Mode

CLEAR(清除影像) [0,0] 
 
PorterDuff.Mode
DARKEN(變暗) [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];
 
PorterDuff.Mode
DST(只繪製目標影像) [Da, Dc]
 
PorterDuff.Mode
DST_ATOP(顯示原影像非交集部分與目標影像交集部分)[Sa, Sa * Dc + Sc * (1 - Da)] 
 
PorterDuff.Mode
DST_IN(顯示原影像與目標影像相交的目標影像部分)[Sa * Da, Sa * Dc]
 
PorterDuff.Mode
DST_OVER(原影像目標影像均顯示,目標影像在上層) [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]
 
PorterDuff.Mode
LIGHTEN(變亮) [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] 
 
PorterDuff.Mode
MULTIPLY(顯示原影像與目標影像交集部分疊加後的顏色)[Sa * Da, Sc * Dc] 
 
PorterDuff.Mode
OVERLAY(疊加) 未知 
 
PorterDuff.Mode
SCREEN(取原影像和目標影像的全部區域,交集部分為透明色) [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] 
 
PorterDuff.Mode
SRC(只顯示原影像) [Sa, Sc] 
 
PorterDuff.Mode
SRC_ATOP(顯示原影像交集部分和目標影像的非交集部分) [Da, Sc * Da + (1 - Sa) * Dc] 
 
PorterDuff.Mode
SRC_IN(顯示原影像與目標影像交集部分的原影像) [Sa * Da, Sc * Da] 
 
PorterDuff.Mode
SRC_OUT(顯示原影像與目標影像非交集部分的目標影像) [Sa * (1 - Da), Sc * (1 - Da)] 
 
PorterDuff.Mode
SRC_OVER(在目標影像的頂部繪製源影像) [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 
 
PorterDuff.Mode
XOR(去除兩圖層交集部分) [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] 
 
PorterDuff.Mode
DST_OUT(只在源影像和目標影像相交的地方繪製源影像) [Da * (1-Sa), Dc * (1-Sa)] 

 

這就是18種PorterDuffMode的18種情況,相關的具體樣式我就不在這裡貼出來了.

public class XfermodeView extends View {

    /**
     * 18種圖形混合模式
     */
    private static final PorterDuff.Mode PorterDuffMode[] = {PorterDuff.Mode.ADD, PorterDuff.Mode.CLEAR, PorterDuff.Mode.DARKEN,
            PorterDuff.Mode.DST, PorterDuff.Mode.DST_ATOP, PorterDuff.Mode.DST_IN, PorterDuff.Mode.DST_OUT, PorterDuff.Mode.DST_OVER,
            PorterDuff.Mode.LIGHTEN, PorterDuff.Mode.MULTIPLY, PorterDuff.Mode.OVERLAY, PorterDuff.Mode.SCREEN, PorterDuff.Mode.SRC,
            PorterDuff.Mode.SRC_ATOP, PorterDuff.Mode.SRC_IN, PorterDuff.Mode.SRC_OUT, PorterDuff.Mode.SRC_OVER, PorterDuff.Mode.XOR};

    private PorterDuffXfermode porterDuffXfermode;

    private int mode;

    private int defaultMode = 0;

    private static final int Layers = 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;

    /**
     * 螢幕寬高
     */
    private int screenW;
    private int screenH;
    private Bitmap srcBitmap;
    private Bitmap dstBitmap;

    /**
     * 源圖和目標圖寬高
     */
    private int width = 120;
    private int height = 120;

    public XfermodeView(Context context) {
        super(context);

    }

    public XfermodeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XfermodeView);
        mode = typedArray.getInt(R.styleable.XfermodeView_ModeNum, defaultMode);
        screenW = ScreenUtil.getScreenW(context);
        screenH = ScreenUtil.getScreenH(context);
        porterDuffXfermode = new PorterDuffXfermode(PorterDuffMode[mode]);
        srcBitmap = makeSrc(width, height);
        dstBitmap = makeDst(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setFilterBitmap(false);
        paint.setStyle(Paint.Style.FILL);
        /**
         * 繪製藍色矩形+黃色圓形
         * */
        canvas.drawBitmap(srcBitmap, screenW / 8 - width / 4, screenH / 12 - height / 4, paint);
        canvas.drawBitmap(dstBitmap, screenW / 2, screenH / 12, paint);

        /**
         * 建立一個圖層,在圖層上演示圖形混合後的效果
         * */
        canvas.saveLayer(0, 0, screenW, screenH, null, Layers);

        /**
         * 繪製在設定了XferMode後混合後的圖形
         * */
        canvas.drawBitmap(dstBitmap, screenW / 4, screenH / 3, paint);
        paint.setXfermode(porterDuffXfermode);
        canvas.drawBitmap(srcBitmap, screenW / 4, screenH / 3, paint);
        paint.setXfermode(null);
        // 還原畫布
        canvas.restore();
    }
}

  這段程式碼針對了18種不同模式顯示的樣式,原影像是一個藍色的正方形,目標影像是一個黃色的圓形.然後我們另起了一個圖層saveLayer(),將這18種模式出現的情況畫在這個新的畫布上.這裡的程式碼並不是完全的,最後我會給出這個程式碼的地址,方便大家理解.

iii.自定義ClipView

 簡單的介紹了一下Canvas和Xfermode,我們就可以使用自定義View,然後結合這二者實現頭像的裁切效果.簡單的說一下原理.

  上面這個圖是實現頭像裁切的原理,我們在Layer層放置一個ImageView,然後入棧一個圖層,將ClipView畫在Layer1上,然後使用Xfermode中的 DST_OUT 模式,這樣取二者的相交部分,也就是ClipView這個圓圈與底部的ImageView的交集部分,並且顯示ImageView部分.其他的地方就變成透明的了.

 

  就像上面這張圖一樣,顯示的地方是二者的交集部分,裁剪框+底部的ImageView的共同部分,然後其他的地方都是透明的.這樣我們就可以只獲取二者交集部分的影像.那麼具體如何實現這裡,需要我們去自定義View實現.在上層的Layer需要自定義一個ClipView.這個View相對而言還是非常簡單的.只需要在新的圖層上用Paint畫一個圓圈和圓邊框就行了.然後設定Xfermode就可以輕鬆的實現.

   public ClipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        /**
         * 去掉鋸齒
         * */
        mPaint.setAntiAlias(true);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setColor(Color.WHITE);
        borderPaint.setStrokeWidth(clipBorderWidth);
        borderPaint.setAntiAlias(true);
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        width = this.getWidth();
        height = this.getHeight();

        /**
         * 另啟一個圖層,以後所有的繪製操作都在此圖層上完成.
         * 不另外開啟一個圖層的話,Canvas在被掏空之後,背景色就不是透明的,而是黑的
         * */
        canvas.saveLayer(0, 0, width, height, null, LAYER_FLAGS);
        canvas.drawColor(Color.parseColor("#a8000000"));
        mPaint.setXfermode(xfermode);
        /**
         * 在畫布上畫透明的圓
         * */
        canvas.drawCircle(width / 2, height / 2, width * radiusWidthRatio, mPaint);
        /**
         * 圓邊框
         * */
        canvas.drawCircle(width / 2, height / 2, width * radiusWidthRatio + clipBorderWidth, borderPaint);
        /**
         * 出棧,恢復到之前的圖層,意味著新建的圖層會被刪除,新建圖層上的內容會被繪製到canvas
         * */
        canvas.restore();
    }

  這裡就不貼全部程式碼了,直接把核心程式碼貼上出來就夠了.程式碼和上面所說的思想基本是一致的.並且還有相關的註釋,我就不多做解釋了.這樣我們也是僅僅實現了在新的Layer上畫了這樣一個圓圈.那麼如何實現縮放和平移圖片,然後獲取到圖片這才是比較重要的一個部分.

 既然要實現縮放和平移,那麼必須要重寫手勢事件.這基本是習以為常的事情了.先貼程式碼,然後再說其中的道理.

@Override
    public boolean onTouch(View v, MotionEvent event) {

        ImageView view = (ImageView) v;
        switch (event.getAction() & MotionEvent.ACTION_MASK) {

            case MotionEvent.ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                /**
                 * 如果間距大於10f,表示以後再MOVE的時候要進行縮放操作,而不是平移操作
                 * */
                if (oldDist > 10f) {
                    saveMatrix.set(matrix);
                    midPoint(midPoint, event);
                    mode = ZOOM;
                }
                break;
            case MotionEvent.ACTION_DOWN:
                /**
                 * 單指觸發按下事件
                 * */
                saveMatrix.set(matrix);
                startPoint.set(event.getX(), event.getY());
                mode = DRAG;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * 拖動
                 * */
                if (mode == DRAG) {
                    matrix.set(saveMatrix);
                    matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y);
                } else if (mode == ZOOM) {
                    float newDist = spacing(event);
                    /**
                     * 如果兩指拉開的間距大於10f那麼就表示需要縮放
                     * */
                    if (newDist > 10f) {
                        matrix.set(saveMatrix);
                        float scale = newDist / oldDist;
                        matrix.postScale(scale, scale, midPoint.x, midPoint.y);
                    }
                }
                break;
        }
        /**
         * 每次操作結束後都需要設定matrix
         * */
        view.setImageMatrix(matrix);
        return true;
    }

  這裡不難發現重寫的事件要比以前多一些,因為這裡不僅僅涉及到我們單指按下,單指按下螢幕一般就是平移圖片,那麼縮放的時候需要多指按下圖片,通過拖動的方式實現縮放功能.

 這裡定義了三個標記位,一個是NONE表示沒有進行任何的操作,DRAG則表示拖動操作,ZOOM表示縮放操作。

 當我們單指按下的時候,首先需要記錄下當前按下的座標,然後改變標誌位,因為單指按下一般後續就是DRAG操作,不可能發生ZOOM操作,因此在ACTION_DOWN之後需要改變標誌位為DRAG如果我們後續進行了平移操作,也就是ACTION_MOVE 那麼就會進行相關的處理,他會根據DRAG或ZOOM操作執行不同的邏輯,如果是DRAG,那麼我們只需要根據移動後的座標和起始按下的座標對view進行平移操作就可以了,這個操作由matrix來決定.

 當我們兩個手指同時按壓到螢幕上的時候,這裡做了一個簡單的判斷,就是兩指之間的距離,如果距離小於10f,那麼就還是表示要執行平移操作,否則執行縮放操作,那麼當需要執行縮放操作的時候首先需要記錄兩指按下的中心點座標,然後根據初始兩指之間的距離和移動後兩指之間的距離做除法運算,就可以計算出我們具體要縮放多少,縮放就是通過根據最開始的中心點以及matrix的配合實現縮放效果.最後基本就是獲取圖片隨機生成一個uri返回就可以了.

 需要注意一點就是圖片在放置到ImageView上的時候我們是需要對圖片進行加工的,因為我們現在手機內部的圖片已經不僅僅是720*1280那麼簡單了現在手機拍攝出來的圖片畫素一般是4000+*3000+的,這個取決於我們相機的畫素,和螢幕的解析度是沒有什麼關係的,因此在篩選完圖片之後就需要對圖片進行相關的處理.因此我為ClipView註冊了一個檢視樹監聽,也就是說當ClipView監聽到整個檢視樹狀態發生了相關的變化,那麼就表示圖片需要顯示在ImageView上了,這時我們就需要對圖片進行加工處理.每一個Layout都構成一個檢視樹,其實我感覺它和DOM樹結構差不多,都是按層級劃分的.還有註冊完之後,觸發的同時需要remove掉,否則會多次呼叫.

ViewTreeObserver observer = clipView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        clipView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        initSrc();
    }
});

  這樣就通過Canvas+Xfermode+自定義ClipView實現了頭像的裁切.裁切出來是個矩形的,只需要顯示在圓形的ImageView上就可以了.這裡圓形的頭像大家也可以選擇其他的類庫,或者是自己自定義.我這裡就不多說了,我是使用的第三方.最後貼一下原始碼方便大家的理解.

 Canvas:http://pan.baidu.com/s/1mhSnkPM

 XferMode:http://pan.baidu.com/s/1dES108T

 圓形頭像裁切:http://pan.baidu.com/s/1nvo5ORR

 

相關文章