使用PorterDuff解決clipPath無法抗鋸齒問題

GitLqr發表於2018-03-16

一、簡述

前段時間公司史無前例的接了一個大資料外包專案(哇~我們又不是外包公司(╯°Д°)╯︵ ┻━┻),要求搞很多圖表方便觀察運營的資料情況,圖表當然要用到MPAndroidChart啦,但並不是所有的圖表都可以用它用實現,這時就需要自定義View了,其中有一個要求,如下圖所示,這就是本篇要實現的效果:

使用PorterDuff解決clipPath無法抗鋸齒問題

本篇全文適合像我一樣的小白細細觀看,如果你很趕時間,就只是進來看看標題上的解決方案,那麼請直接看第二部分分析與實現的第5章節《優化解決抗鋸齒問題》

二、分析與實現

最終效果上圖就可以看到了,下面就來想想怎麼實現從0實現這個自定義View吧。

1、分析

1)UI

可以看到這個View要根據進度,繪製對應長度的弧(或者說是不完整的環),因為環的顏色是漸變的,不能用程式來控制(或者說不好實現),所以向美術要了如下兩張切圖:

使用PorterDuff解決clipPath無法抗鋸齒問題

使用PorterDuff解決clipPath無法抗鋸齒問題

別看這兩個環大小不一,實際上圖片的整體尺寸是一樣的,都是95*95。

那麼接下來就是根據進度把圖片的部分割槽域繪製出來就好了。

2)功能

  1. 可以自由設定總進度(maxProcess)與當前進度(process)。
  2. 可以執行繪製動畫效果。

3)難點

  1. canvas繪製bitmap
  2. canvas裁切功能的使用
  3. 鋸齒出現的原因與解決方法
  4. PorterDuff的理解與使用

2、實現

1)獲取自定義控制元件寬高

這段可以說是自定義View的模板程式碼了,就不詳細說明,基本上所有的自定義View都這樣測量控制元件寬高,模板程式碼如下:

public class ArithmeticView extends View {
    private int mWidth  = 0;// 控制元件的寬度
    private int mHeight = 0;// 控制元件的高度
	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
		switch (widthSpecMode) {
            case MeasureSpec.AT_MOST:
                mWidth = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mWidth = widthSpecSize;
                break;
        }
		switch (heightSpecMode) {
            case MeasureSpec.AT_MOST:
                mHeight = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mHeight = heightSpecSize;
                break;
        }
        setMeasuredDimension(mWidth, mHeight);
    }
}
複製程式碼

2)初始化載入圖片資源

因為是專案特有的自定義View,不考慮通用問題,直接在View建立後載入需要用到的圖片資源即可。

private Bitmap mBitmapInner;
private Bitmap mBitmapOuter;

public ArithmeticView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}    
private void init() {
    mBitmapInner = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_inner);
    mBitmapOuter = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_outer);
}
複製程式碼

3)draw()方法實現

因為要繪製的環有2個,分為大小環,故繪製對應環的方法命名為:drawInnerBitmap()、drawOuterBitmap()。

@Override
protected void onDraw(Canvas canvas) {
    canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
    drawInnerBitmap(canvas);
    drawOuterBitmap(canvas);
}

private void drawInnerBitmap(Canvas canvas) {
}
private void drawOuterBitmap(Canvas canvas) {
}
複製程式碼

可以看到在onDraw()方法中有這樣一段程式碼:

canvas.setDrawFilter(new PaintFlagsDrawFilter(0, >Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));

這是為了抗鋸齒的,但這裡先透露一下,該方法對下面的實現方案一抗鋸齒無效。

3、canvas繪製bitmap

1)canvas.drawBitmap()

canvas繪製bitmap用到的方法就是drawBitmap(),它的所有過載方法如下圖所示

使用PorterDuff解決clipPath無法抗鋸齒問題

說明一下,如果你就單單只是把繪製Bitmap繪製出來,那麼最後的paint引數可以傳入null。

實際上,當你使用drawBitmap()繪製Bitmap時,畫筆paint的作用並不大,可以認為無效。

那麼多方法,用哪個呢?其實開發中常用的過載方法就如下兩個:

public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, @Nullable Paint paint)
複製程式碼

第1個過載方法引數較少,其中left和top表示圖片要繪製到canvas的起始位置,這個方法無法指定bitmap的圖片要顯示的區域(有時一張圖片就只需要顯示它的四分之一),這就是該過載方法的侷限,而第2個過載方法則可以隨便指定bitmap要顯示的區域,而且是最常用的方法,功能相對更強,這裡使用該方法來實現Bitmap的繪製。

2)繪製外環Bitmap

因為2個環的繪製原理一樣,所以這裡就以繪製外環為例:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);
	canvas.drawBitmap(mBitmapOuter, src, dsc, null);
}
複製程式碼

其中,src表示要圖片要被繪製區域(注意,是針對圖片來說的),如果只繪製圖片的四分之一,則程式碼如下:

int left = 0;
int top = 0;
int right = mBitmapOuter.getWidth() / 2;
int bottom = mBitmapOuter.getHeight() / 2;
Rect src = new Rect(left, top, right, bottom);
複製程式碼

而dsc則表示要繪製到canvas上的哪個矩形區域(注意,是針對canvas來說的),前面的懂了,相信這個也不難理解。

使用PorterDuff解決clipPath無法抗鋸齒問題

這樣,圖片就繪製出來了,注意此時是沒有鋸齒的。

4、方案一:使用canvas裁切功能

環的完整繪製在上面已經用drawBitmap()方法實現,那麼接下來就是繪製一個不完整的環了。我首先想到的方法就是使用canvas的裁切功能,將canvas的繪製區域先裁切出來,然後再在上面繪製圖形,進而實現根據進度繪製圖片的一部分。

針對該自定義View,需要說明一點,canvas的可繪製區域應該是一個從中上方開始逆時針開啟的扇形(可以想象成一把扇子);用二維座標系的方式來說的話,就從y軸開始,以原點為圓心,逆時針畫圓。

1)canvas.clipXXX()

canvas的裁切功能需要用到clip開頭的方法,canvas中所有的clipXXX()方法如下圖所示:

使用PorterDuff解決clipPath無法抗鋸齒問題

絕大部分方法都是為了裁切出一個矩形,而我們這個不一樣,它是要裁切出一個扇形!所以只有一個clipPath()方法可用,由我們來自定義裁切形狀(同樣的,除了矩形以外的任何形狀都需要我們自己定義路徑)。

2)使用clipPath()根據進度裁切扇形

不管是哪個clipPath()方法,都需要用到Path物件,該path物件就代表了canvas的裁切路徑,因為大小環的進度可能不同,但原理一樣,所以將path的設定程式碼抽出來作為一個共用方法,程式碼如下:

private Path   mPath;
private void init() {
	...
    mPath = new Path();
}
/**
 * 設定裁切路徑
 *
 * @param process    當前進度
 * @param maxProcess 總進度
 */
private void setClipPath(float process, float maxProcess) {
    mPath.reset();
    float ratio = process / maxProcess;
    if (ratio == 1) {
		// 當進度比例為1時,說明進度100%,要完整繪製Bitmap
        mPath.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
    } else {
        float sweepAngle = ratio * -360;
        mPath.moveTo(mWidth / 2, mHeight / 2);// View的中心點位置
        mPath.lineTo(mWidth / 2, mHeight);// View的中心點上方位置
        mPath.arcTo(new RectF(0, 0, mWidth, mHeight), 270, sweepAngle * mProgressPercent, false);// 根據角度畫弧線
        mPath.lineTo(mWidth / 2, mHeight / 2);// 最後再回到View的中心點位置,形成一個封閉路徑
    }
    mPath.close();
}
複製程式碼

關於Path的moveTo()、lineTo()、arcTo等方法在這裡就是不詳細科普了,因為方法比較多,要說明可能會花費不少篇幅,而且實際上這些方法顧名就可思義,如果不是很懂的同學自行百度查一下吧。

這裡要著重說明一點,Android中的角度問題,如下圖所示:

使用PorterDuff解決clipPath無法抗鋸齒問題

Android中的角度是以x軸為0°開始,以順時針方向遞增,而我們的自定義View要繪製的方向則是逆時針,所以要計算arcTo的sweepAngle時要注意乘以一個負值。

3)canvas的儲存與復原

要注意一點,canvas的裁切功能會對後續的繪製產生影響,所以在裁切之前需要將Canvas的當前狀態儲存一下,在裁切繪製過後將Canvas的狀態恢復回來。否則,之後的繪製結果可能並不是你想要的了。

如果不儲存與恢復Canvas的狀態,那麼下次繪製只會在裁切出來的區域中進行。舉個例子,假設內環的尺寸只有50*50,你先繪製了內環,但沒有儲存與恢復Canvas的狀態,那麼之後在繪製外環時(外環尺寸95*95),你就會發現外環看不到了。

儲存Canvas的當前狀態程式碼:

canvas.save(Canvas.CLIP_SAVE_FLAG);

恢復Canvas之前的狀態程式碼:

canvas.restore();

所以外環裁切並繪製的完整程式碼如下:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

    canvas.save(Canvas.CLIP_SAVE_FLAG);
    setClipPath(mOuterProcess, mOuterMaxProcess);
    canvas.clipPath(mPath);
    canvas.drawBitmap(mBitmapOuter, src, dsc, null);
    canvas.restore();
}
複製程式碼

將進度設定為80%後,繪製出來的效果如下:

使用PorterDuff解決clipPath無法抗鋸齒問題

注意了,鋸齒出現了!!!

5、優化解決抗鋸齒問題

為什麼?明明對canvas設定了抗鋸齒了,怎麼還這樣?難道是因為在呼叫canvas.drawBitmap()時,沒有傳入抗鋸齒畫筆的原因?錯,前面已經說過了,這個paint對drawBitmap()的作用並不大,就算你傳入了可以抗鋸齒的paint,鋸齒依然存在。仔細想想,在使用裁切前後,鋸齒的出現情況,你就能發現貓膩,百度及Google大法後,最終得出如下幾點結論:

  • Android中,Canvas的抗鋸齒功能必須使用到畫筆Paint。
  • clipPath()會使drawBitmap()繪製出來的影象出現鋸齒,而且沒法解決方法,即使你使用了畫筆。
  • 可以使用PorterDuff來實現同樣的繪製效果。

那麼,接下來就開始進行“曲線救國”路線。

6、方案二:使用PorterDuff

對於PorterDuff的介紹這裡就不說了,百度吧,或者跳過,直接來看下面這圖:

使用PorterDuff解決clipPath無法抗鋸齒問題

這圖很好的表示了PorterDuff.Mode的各種效果,下面是對效果的詳細說明。

Mode 說明
.CLEAR 所繪製不會提交到畫布上
PorterDuff.Mode.SRC 顯示上層繪製圖片
PorterDuff.Mode.DST 顯示下層繪製圖片
PorterDuff.Mode.SRC_OVER 正常繪製顯示,上下層繪製疊蓋
PorterDuff.Mode.DST_OVER 上下層都顯示。下層居上顯示
PorterDuff.Mode.SRC_IN 取兩層繪製交集。顯示上層
PorterDuff.Mode.DST_IN 取兩層繪製交集。顯示下層
PorterDuff.Mode.SRC_OUT 取上層繪製非交集部分
PorterDuff.Mode.DST_OUT 取下層繪製非交集部分
PorterDuff.Mode.SRC_ATOP 取下層非交集部分與上層交集部分
PorterDuff.Mode.DST_ATOP 取上層非交集部分與下層交集部分
PorterDuff.Mode.XOR 異或:去除兩圖層交集部分
PorterDuff.Mode.DARKEN 取兩圖層全部區域,交集部分顏色加深
PorterDuff.Mode.LIGHTEN 取兩圖層全部,點亮交集部分顏色
PorterDuff.Mode.MULTIPLY 取兩圖層交集部分疊加後顏色
PorterDuff.Mode.SCREEN 取兩圖層全部區域,交集部分變為透明色

仔細看圖中的Src、Dst、SrcIn,有沒有什麼想法呢?

使用PorterDuff解決clipPath無法抗鋸齒問題

如果你之前沒用過PorterDuff.Mode也不慌,繼續"聽"我給你吹水,並不難理解。

1)原理與實現

這裡要先理解2個單詞:src與dst。通俗的說,dst是已經繪製在canvas上的影象,而src則是將要繪製到canvas上的影象(不得不說跟OpenGL的模板測試部分概念很像耶~)。那麼,對於我們這個自定義View而言,dst就是那個扇形影象,src就是環,再結合PorterDuff.Mode的SrcIn模式,就可以讓環的Bitmap只顯示出扇形的部分了。

注意:使用PorterDuff需要禁止硬體加速。

程式碼很簡單,不多廢話,如下:

private Paint         mPaint;
private void init() {
    // 禁止硬體加速,硬體加速會有一些問題,這裡禁用掉
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
}
private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

    mPaint.reset();
    setClipPath(mOuterProcess, mOuterMaxProcess);
    canvas.drawPath(mPath, mPaint);// 繪製Dst
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 設定轉換模式(顯示Scr與Dst交接的區域)
    canvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 繪製Src
}
複製程式碼

效果很棒,沒有鋸齒了。

使用PorterDuff解決clipPath無法抗鋸齒問題

2)優化,實現雙環無鋸齒繪製

前面看似已經用PorterDuff實現了環的無鋸齒繪製,但如果內環也是按照上面的程式碼來寫,你就會發現,只顯示後繪製的外環。具體原因嘛,我也不太懂,猜測是多次使用PorterDuff,會將之前繪製在Canvas上的影象清除吧。(如果有人知道真實原因,麻煩留言說一下,thx)。既然,直接對canvas操作不可取,那就換個思路吧。

使用PorterDuff解決clipPath無法抗鋸齒問題

我們要的不過是最後顯示出來的不完整的環對吧,那我們可以在另一個Canvas上把這個不完整的環畫出來,得到它的Bitmap,再在onDraw()的Canvas上對這個Bitmap進行繪製即可。所以,改進後的程式碼如下:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

	// 1. 在另一個Canvas中使用 path + mBitmapOuter 將最終圖形finalBitmap繪製出來。
    mPaint.reset();
    setClipPath(mOuterProcess, mOuterMaxProcess);
    Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一張白紙點陣圖
    Canvas mCanvas = new Canvas(finalBitmap);// 用指定的點陣圖構造一個畫布來繪製
    mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 畫布繪製Bitmap時搞鋸齒
    mCanvas.drawPath(mPath, mPaint);// 繪製Dst
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 設定轉換模式(顯示Scr與Dst交接的區域)
    mCanvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 繪製Src
    // 2. 再在原來的Canvas中將finalBitmap繪製出來。
    canvas.drawBitmap(finalBitmap, 0, 0, null);
}
複製程式碼

同樣的,內環的程式碼基本一致,直接看效果吧。

使用PorterDuff解決clipPath無法抗鋸齒問題

三、完整程式碼

至於那個執行繪製動畫效果的功能並非本文重點,實現上能見仁見智,下面貼出該自定義View的完整程式碼,當然執行繪製動畫效果的功能也在裡面(具體實現看animateStart()方法)。完整程式碼如下:

public class ArithmeticView extends View {

    private int mWidth  = 0;// 控制元件的寬度
    private int mHeight = 0;// 控制元件的高度
    private Bitmap mBitmapInner;
    private Bitmap mBitmapOuter;
    private Path   mPath;
    private float mInnerProcess    = 50;
    private float mInnerMaxProcess = 100;
    private float mOuterProcess    = 80;
    private float mOuterMaxProcess = 100;
    private float mProgressPercent = 1;// 當前進度百分比
    private ValueAnimator mValueAnimator;
    private Paint         mPaint;

    public ArithmeticView(Context context) {
        this(context, null);
    }

    public ArithmeticView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ArithmeticView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        switch (widthSpecMode) {
            case MeasureSpec.AT_MOST:
                mWidth = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mWidth = widthSpecSize;
                break;
        }
        switch (heightSpecMode) {
            case MeasureSpec.AT_MOST:
                mHeight = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mHeight = heightSpecSize;
                break;
        }
        setMeasuredDimension(mWidth, mHeight);
    }

    private void init() {
        // 禁止硬體加速,硬體加速會有一些問題,這裡禁用掉
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mBitmapInner = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_inner);
        mBitmapOuter = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_outer);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
        drawInnerBitmap(canvas);
        drawOuterBitmap(canvas);
    }

    private void drawInnerBitmap(Canvas canvas) {
        int left = 0;
        int top = 0;
        int right = mBitmapInner.getWidth();
        int bottom = mBitmapInner.getHeight();
        Rect src = new Rect(left, top, right, bottom);
        RectF dsc = new RectF(0, 0, mWidth, mHeight);

        mPaint.reset();
        setClipPath(mInnerProcess, mInnerMaxProcess);
        Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一張白紙點陣圖
        Canvas mCanvas = new Canvas(finalBitmap);// 用指定的點陣圖構造一個畫布來繪製
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 畫布繪製Bitmap時搞鋸齒
        mCanvas.drawPath(mPath, mPaint);// 繪製Dst
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 設定轉換模式(顯示Scr與Dst交接的區域)
        mCanvas.drawBitmap(mBitmapInner, src, dsc, mPaint);// 繪製Src
        canvas.drawBitmap(finalBitmap, 0, 0, null);
    }

    private void drawOuterBitmap(Canvas canvas) {
        int left = 0;
        int top = 0;
        int right = mBitmapOuter.getWidth();
        int bottom = mBitmapOuter.getHeight();
        Rect src = new Rect(left, top, right, bottom);
        RectF dsc = new RectF(0, 0, mWidth, mHeight);

        /*------------------ 方案1:使用clipPath方式繪製圖片,但無法抗鋸齒 ------------------*/
//        canvas.save(Canvas.CLIP_SAVE_FLAG);
//        setClipPath(mOuterProcess, mOuterMaxProcess);
//        canvas.clipPath(mPath);
//        canvas.drawBitmap(mBitmapOuter, src, dsc, null);
//        canvas.restore();

        /*------------------ 方案二:使用PorterDuff方式,可以抗鋸齒 ------------------*/
        // 1. 在另一個Canvas中使用 path + mBitmapOuter 將最終圖形finalBitmap繪製出來。
        mPaint.reset();
        setClipPath(mOuterProcess, mOuterMaxProcess);
        Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一張白紙點陣圖
        Canvas mCanvas = new Canvas(finalBitmap);// 用指定的點陣圖構造一個畫布來繪製
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 畫布繪製Bitmap時搞鋸齒
        mCanvas.drawPath(mPath, mPaint);// 繪製Dst
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 設定轉換模式(顯示Scr與Dst交接的區域)
        mCanvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 繪製Src
        // 2. 再在原來的Canvas中將finalBitmap繪製出來。
        canvas.drawBitmap(finalBitmap, 0, 0, null);
    }

    /**
     * 設定裁切路徑
     *
     * @param process    當前進度
     * @param maxProcess 總進度
     */
    private void setClipPath(float process, float maxProcess) {
        mPath.reset();
        float ratio = process / maxProcess;
        if (ratio == 1) {
            mPath.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
        } else {
            float sweepAngle = ratio * -360;
            mPath.moveTo(mWidth / 2, mHeight / 2);
            mPath.lineTo(mWidth / 2, mHeight);
            mPath.arcTo(new RectF(0, 0, mWidth, mHeight), 270, sweepAngle * mProgressPercent, false);
            mPath.lineTo(mWidth / 2, mHeight / 2);
        }
        mPath.close();
    }

    public float getInnerProcess() {
        return mInnerProcess;
    }

    public void setInnerProcess(float innerProcess) {
        mInnerProcess = innerProcess;
        postInvalidate();
    }

    public float getInnerMaxProcess() {
        return mInnerMaxProcess;
    }

    public void setInnerMaxProcess(float innerMaxProcess) {
        mInnerMaxProcess = innerMaxProcess;
    }

    public float getOuterProcess() {
        return mOuterProcess;
    }

    public void setOuterProcess(float outerProcess) {
        mOuterProcess = outerProcess;
        postInvalidate();
    }

    public float getOuterMaxProcess() {
        return mOuterMaxProcess;
    }

    public void setOuterMaxProcess(float outerMaxProcess) {
        mOuterMaxProcess = outerMaxProcess;
    }

    /**
     * 開始動畫
     *
     * @param duration 動畫時長(毫秒)
     */
    public void animateStart(long duration) {
        if (mValueAnimator != null) {
            mValueAnimator.cancel();
            mValueAnimator = null;
        }
        mValueAnimator = ValueAnimator.ofFloat(0, 100);
        mValueAnimator.setDuration(duration).start();
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Float value = (Float) animation.getAnimatedValue();
                if (value == 100) {
                    mValueAnimator.cancel();
                    mValueAnimator = null;
                }
                mProgressPercent = value / 100;
                postInvalidate();
            }
        });
    }

    private int dp2px(int dp) {
        float density = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * density);
    }
}
複製程式碼

相關文章