Android之圓形頭像裁切

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

 

學習內容:canvas

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

 

  頭像裁切現現在在不少應用中都會獲得使用,通常都是在我的資料頁面設置頭像,而後選擇圖片,或者是直接開啓相機拍攝一張圖片,經過裁切和縮放的手段最後顯示在ImageView上就能夠了.不過不管怎樣裁切其實最後裁切出來仍然是一個方形的圖片,所以咱們須要自定義ImageView,將ImageView定義成咱們想要的形狀,而後將裁切到的圖片顯示到ImageView上就能夠了.這裏的ImageView我是使用的第三方框架,由於本身尚未打算自定義ImageView這塊.所以就直接用了別人的東西.app

 

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

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

 

  Canvas的結構基本是這樣的,在View繪製到屏幕上的時候在OnDraw()方法在調用的時候,全部的控件就會經過Paint繪製到Canvas上.其實就是畫到畫布上,默認狀況下,咱們能夠把Canvas也看做成一個Layer,當咱們在saveLayer()的時候,就表示咱們開啓一個新的圖層,全部繪製的內容都會在當前圖層完成,不會影響到前一張圖層,至關於圖層的覆蓋,當咱們調用restore()方法的時候,那麼當前圖層出棧,將全部的內容繪製到被覆蓋的圖層.簡單的說說saveLayer()方法如何使用.post

 這裏咱們在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實現複雜圖形,所以先在這裏簡單的介紹一下,以避免在後續看到這塊代碼不知道具體是要作什麼用的.this

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

相關文章
相關標籤/搜索