Android - ValueAnimator+PathMeasure實現支付寶支付動畫

我的CSDN: ListerCi
我的簡書: 東方未曦

一、效果展示

動畫分爲三種狀態:Loading、Success、Fail,可以點擊按鈕切換狀態。
加載後成功的效果如下所示。
Loading->Success
加載後失敗的效果如下所示。
Loading->Fail

二、前置知識

1. ValueAnimator

ValueAnimator是屬性動畫的一種,它不直接改變View的屬性,而是不斷生成一個代表動畫進度的值,用戶通過該值改變View的某些屬性達到動畫的效果。ValueAnimator基本的用法如下。

ValueAnimator anim = ValueAnimator.ofFloat(0, 1); // 動畫的進度爲[0, 1]中某個值
anim.setDuration(1000); // 動畫的時長爲 1s
anim.setRepeatMode(ValueAnimator.RESTART); // 動畫重複時重新開始
anim.setRepeatCount(ValueAnimator.INFINITE); // 動畫重複次數爲無限次
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        progress = (float) animation.getAnimatedValue(); // 獲取到動畫的當前進度
        invalidate(); // 立即重繪當前 View
    }
});
anim.start();

程序通過爲ValueAnimator設置監聽來獲取當前的進度progress,這裏的進度爲[0, 1]中的某個float值。如果有一個這樣的需求,要求某個View從透明慢慢變爲顯示,就可以通過view.setAlpha(progress * 255)來實現這種淡出的效果。
除了AnimatorUpdateListener,ValueAnimator還有個監聽器AnimatorListener,主要用於對動畫的狀態進行監聽,4個方法如下。

public void start() { } // 動畫開始時調用
public void end() { } // 動畫結束時調用
public void cancel() { } // 動畫取消時調用
public void repeat() { } // 動畫重複時調用

2. PathMeasure

PathMeasure用於實現路徑動畫,它能夠截取Path中的一段內容進行顯示。構造方法如下。
參數中的path就是PathMeasure 之後截取的對象,forceClosed只對測量PathMeasure長度的結果有影響,一般設置爲false。

PathMeasure pm = new PathMeasure(Path path, boolean forceClosed);

PathMeasure的常用函數如下。

float getLength(); // 獲取當前段的長度(注意是當前)
boolean nextContour(); // 跳轉到Path的下一條曲線,成功返回true
/**
 * 通過startD和stopD來截取Path中的某個片段
 * 結果保存至dst
 * startWithMoveTo爲true時,截取的path保存原樣
 */
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo);

如果你還不理解也不要緊,下面讓我們在實戰中去理解ValueAnimator和PathMeasure。

三、Loading動畫

首先來完成Loading動畫,該動畫的主體是一個圓,在動畫的過程中不斷對圓切割,展示其中的一部分。實現加載框的有種做法是在View每次調用onDraw(Canvas)時調整圓弧的起始角度(startAngle)和掃過的角度(sweepAngle),再根據角度繪製Arc圓弧,但是這種做法有3個問題。第一是不方便控制動畫的時間,也就是duration; 第二是不方便設置插值器,導致動畫一直爲勻速;第三是無法對動畫的各種狀態(開始、結束等)進行監聽。
而ValueAnimator正好能解決上述問題,我們通過ValueAnimator計算動畫的進度,再通過PathMeasure切割圓弧。我們一步一步來,先嚐試把簡單的圓畫出來。
自定義一個PayTestView ,在其中新建一個ValueAnimator,該動畫的進度從0到1,並設置爲無限循環,在監聽器的onAnimationUpdate()方法中將動畫的進度賦值給mProgress。隨後新建一個PathMeasure,因爲所要截取的動畫爲一個圓,所以新建PathMeasure時傳入一個圓形Path。

public class PayTestView extends View {

    private float mProgress; // 代表動畫當前進度

    private Paint mBluePaint; // 藍色畫筆

    private ValueAnimator mLoadingAnimator;
    private PathMeasure mLoadingPathMeasure;
    private Path mDstPath; // 保存PathMeasure切割後的內容

    public PayTestView(Context context) {
        super(context);
        init();
    }

    public PayTestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null); // 取消硬件加速
        // 畫筆設置
        mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 畫筆抗鋸齒
        mBluePaint.setColor(Color.BLUE);
        mBluePaint.setStyle(Paint.Style.STROKE);
        mBluePaint.setStrokeWidth(10);
        mBluePaint.setStrokeCap(Paint.Cap.ROUND);
        // 新建 PathMeasure
        Path loadingPath = new Path();
        loadingPath.addCircle(100, 100, 60, Path.Direction.CW); // CW代表順時針
        mLoadingPathMeasure = new PathMeasure(loadingPath, false);
        mDstPath = new Path();
        // 動畫
        mLoadingAnimator = ValueAnimator.ofFloat(0, 1);
        mLoadingAnimator.setDuration(1500);
        mLoadingAnimator.setRepeatMode(ValueAnimator.RESTART);
        mLoadingAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mLoadingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mProgress = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        mLoadingAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDstPath.reset();
        float stop = mLoadingPathMeasure.getLength() * mProgress;
        mLoadingPathMeasure.getSegment(0, stop, mDstPath, true);
        canvas.drawPath(mDstPath, mBluePaint);
    }
}

可以發現在onDraw()中使用getSegment(start, stop, ...)切割圓弧時,起點start永遠是0,而終點是整個圓弧的長度乘當前進度。每次調用onDraw()時就將從0到當前進度的圓弧切割,因此得到的是圓弧從0度增長到到360度的動畫,並且循環播放。效果如下。
Loading1
當前動畫中圓弧的起點一直是0,最後的效果比較僵硬,我們嘗試修改動畫的起點。這裏使用啓艦《Anroid自定義控件開發入門與實戰》中的方法,在mProgress <= 0.5時,start爲0,在mProgress > 0.5時,start爲mProgress * 2 - 1。修改onDraw()中的代碼如下:

mDstPath.reset();
float length = mPathMeasure.getLength();
float stop = mProgress * length;
float start = (float) (stop - (0.5 - Math.abs(mProgress - 0.5)) * length);
mPathMeasure.getSegment(start, stop, mDstPath, true);

這裏將start的計算放在了一句代碼中,當然用if-else的方法來做可讀性會更高。最終的效果如下。
Loading2
這個效果離之前的展示的Loading動畫已經比較接近了,仔細觀察可以發現,最終的Loading動畫只是在當前的動畫上加了一個整體旋轉的效果。我們可以通過旋轉View的畫布(Canvas)來實現,要注意的是,旋轉時必須按照Loading動畫的圓心進行旋轉。
不過對畫布的旋轉也會影響到之後成功/失敗狀態下的動畫,因此在旋轉之前需要將當前的畫布保存,然後在Loading動畫結束之後恢復畫布。
首先定義一個變量標記畫布是否被保存了,因爲畫布只需要保存一次;隨後在進入onDraw()時判斷畫布是否已經被保存,如果未保存,則保存當前畫布,否則跳過。

public class PayTestView extends View {

    // ......

    private boolean hasCanvasSaved = false; // 畫布是否已被保存
    private int mCurRotate = 0; // 當前畫布旋轉的角度

    // ......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 在 Loading 狀態下 Canvas 會被旋轉, 需要在第一次進入時保存
        if (!hasCanvasSaved) {
            canvas.save();
            hasCanvasSaved = true;
        }
        // Loading 動畫
        // ......
        mCurRotate = (mCurRotate + 2) % 360;
        canvas.rotate(mCurRotate, 100, 100);
        canvas.drawPath(mDstPath, mBluePaint);
    }
}

效果如下所示。
Loading3
此時的Loading動畫已經完成,只不過圓的座標(100, 100)和半徑(60)是固定的。其實我們可以在onSizeChanged()中獲取當前View的高寬,再去設置圓的座標和半徑。這裏不細說,後面會在整體代碼中貼出。

三、狀態切換

整個支付的動畫包含3種狀態:加載、成功、失敗。那麼繪製時怎麼識別當前的狀態?他們之間的狀態又是怎麼切換的呢?
對於第一個問題,我們可以在onDraw(Canvas)中根據動畫當前的狀態來繪製不同的圖形。但是要注意在繪製Loading動畫之前保存畫布;在繪製成功或失敗動畫之前將原始的畫布恢復。onDraw()中的邏輯如下所示。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 在 Loading 狀態下 Canvas 會被旋轉, 需要在第一次進入時保存
    if (!hasCanvasSaved) {
        canvas.save();
        hasCanvasSaved = true;
    }
    // 判斷當前動畫的狀態並繪製相應動畫
    if (curStatus == STATUS_LOADING) {
        // 繪製 Loading 動畫
    } else if (curStatus == STATUS_SUCCESS) {
        // 如果畫布還未恢復則將其恢復
        if (!hasCanvasRestored) {
            canvas.restore();
            hasCanvasRestored = true;
        }
        // 繪製 success 動畫
    } else if (curStatus == STATUS_FAIL) {
        if (!hasCanvasRestored) {
            canvas.restore();
            hasCanvasRestored = true;
        }
        // 繪製 fail 動畫
    }
}

對於第二個問題,狀態之間的切換需要外部調用,且只能由loading向success或fail切換。因此View中需要一個供外部修改狀態的方法,同時修改狀態後需要停止Loading動畫。

// 將動畫的狀態從 Loading 變爲 success 或 fail
public void setStatus(int status) {
    if (curStatus == STATUS_LOADING && status != STATUS_LOADING) {
        curStatus = status;
        mLoadingAnimator.end();
    }
}

Loading動畫結束之後需要開始success動畫或fail動畫,你可以在上述的setStatus()方法中開始success/fail動畫,也可以爲Loading動畫設置監聽,監聽到其結束時開啓新動畫。這裏使用監聽的方式,如下所示。

mLoadingAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) { }

    @Override
    public void onAnimationEnd(Animator animation) {
        if (curStatus == STATUS_SUCCESS) {
            mSuccessAnimator.start();
        } else if (curStatus == STATUS_FAIL) {
            mFailAnimator.start();
        }
    }

    @Override
    public void onAnimationCancel(Animator animation) { }

    @Override
    public void onAnimationRepeat(Animator animation) { }
});

瞭解了3種動畫的切換邏輯之後,再來看看成功/失敗動畫的實現。

四、成功動畫

之前的Loading動畫只有一段路徑,就是一個圓。而成功的動畫包含兩段:外部的圓和內部的勾。之前介紹過PathMeasure是可以通過nextContour()方法從Path中的一段路徑切換到下一段的,因此我們可以構造一個由兩段路徑構成的PathMeasure。

Path successPath = new Path();
successPath.addCircle(100, 100, 60, Path.Direction.CW);
successPath.moveTo(100- 60 * 0.5f, 100- 60 * 0.2f);
successPath.lineTo(100 - 60 * 0.1f, 100 + 60 * 0.4f);
successPath.lineTo(100 + 60 * 0.6f, 100 - 60 * 0.5f);
mSuccessPathMeasure = new PathMeasure(successPath, false);

代碼首先在successPath中添加了外圈的圓,隨後moveTo到勾的起點,lineTo到勾的下方,最後lineTo到勾的終點。很顯然moveTo之前的是第一段路徑,moveTo之後的是第二段路徑。
爲了在ValueAnimator的進度中將兩段路徑分開,新建時進度的範圍設置爲[0, 2]。

mSuccessAnimator = ValueAnimator.ofFloat(0, 2);
// ......

在繪製時,mProgress∈[0, 1]代表外部的圓,mProgress∈[1, 2]代表內部的勾,我們通過一個變量來表示當前在繪製第幾段,初始化時爲1。

private int mSuccessIndex = 1;

繪製代碼如下,當mProgress < 1時繪製外部的圓,mProgress >= 1時切換到下一條路徑。代碼在切換路徑之前通過mSuccessPathMeasure.getSegment(0, mSuccessPathMeasure.getLength(), mSuccessDstPath, true)將第一段路徑完整地繪製了一下。如果不使用這句代碼,第一段的圓繪製出來不是完整的。

if (mProgress < 1) {
    float stop = mSuccessPathMeasure.getLength() * mProgress;
    mSuccessPathMeasure.getSegment(0, stop, mSuccessDstPath, true);
} else {
    if (mSuccessIndex == 1) {
        mSuccessIndex = 2;
        mSuccessPathMeasure.getSegment(0, mSuccessPathMeasure.getLength(), mSuccessDstPath, true);
        mSuccessPathMeasure.nextContour();
    }
    float stop = mSuccessPathMeasure.getLength() * (mProgress - 1);
    mSuccessPathMeasure.getSegment(0, stop, mSuccessDstPath, true);
}
canvas.drawPath(mSuccessDstPath, mBluePaint);

PS:千萬不要在切換路徑時使用if (mProgress == 1)這種寫法,首先動畫的進度值是float類型的,要判斷float值是否“相等”只能用if (Math.abs(mProgress - 1) < 0.01)這種方式;其次如果把動畫執行中的所有進度值打印出來,會是這個樣子的:

2019-06-13 00:00:24.842 2224-2224/com.lister.myviews E/TAG: progress = 0.0
2019-06-13 00:00:24.853 2224-2224/com.lister.myviews E/TAG: progress = 5.57065E-4
2019-06-13 00:00:24.869 2224-2224/com.lister.myviews E/TAG: progress = 0.0022275448
......
2019-06-13 00:00:25.604 2224-2224/com.lister.myviews E/TAG: progress = 0.9411293
2019-06-13 00:00:25.621 2224-2224/com.lister.myviews E/TAG: progress = 0.9725146
2019-06-13 00:00:25.637 2224-2224/com.lister.myviews E/TAG: progress = 1.0058903
2019-06-13 00:00:25.656 2224-2224/com.lister.myviews E/TAG: progress = 1.0392599
2019-06-13 00:00:25.675 2224-2224/com.lister.myviews E/TAG: progress = 1.0706271
2019-06-13 00:00:25.693 2224-2224/com.lister.myviews E/TAG: progress = 1.1038773
......
2019-06-13 00:00:26.407 2224-2224/com.lister.myviews E/TAG: progress = 1.9984891
2019-06-13 00:00:26.423 2224-2224/com.lister.myviews E/TAG: progress = 1.9997668
2019-06-13 00:00:26.440 2224-2224/com.lister.myviews E/TAG: progress = 2.0

進度值progress會在多大的範圍內逼近1.0是無法確定的,因此直接判斷進度值是不是小於1比較妥當。

五、完整代碼

fail狀態下的動畫比較簡單,是一個三段的路徑,不再贅述。這裏貼出完整代碼,註釋也比較齊全,如果有不完善的地方還望批評指正。

public class PayAnimatorView extends View {

    /**
     * 動畫狀態:加載中、成功、失敗
     */
    public static final int STATUS_LOADING = 1;
    public static final int STATUS_SUCCESS = 2;
    public static final int STATUS_FAIL = 3;

    /**
     * 當前動畫的狀態
     */
    private int curStatus;

    /**
     * loading 動畫變量
     */
    private PathMeasure mPathMeasure;
    private Path mDstPath;
    private int mCurRotate = 0;
    private float mProgress;
    private boolean hasCanvasSaved = false;
    private boolean hasCanvasRestored = false;

    /**
     * success / Fail 動畫變量
     */
    private PathMeasure mSuccessPathMeasure;
    private Path mSuccessDstPath;
    private PathMeasure mFailPathMeasure;
    private Path mFailDstPath;

    /**
     * 動畫
     */
    private ValueAnimator mLoadingAnimator;
    private ValueAnimator mSuccessAnimator;
    private ValueAnimator mFailAnimator;

    private Paint mBluePaint;
    private Paint mRedPaint;

    private int mCenterX, mCenterY;

    public PayAnimatorView(Context context) {
        super(context);
        init();
    }

    public PayAnimatorView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterX = w / 2;
        mCenterY = h / 2;
        int radius = (int) (Math.min(w, h) * 0.3);
        // 在獲取寬高之後設置加載框的位置和大小
        Path circlePath = new Path();
        circlePath.addCircle(mCenterX, mCenterY, radius, Path.Direction.CW);
        mPathMeasure = new PathMeasure(circlePath, true);
        mDstPath = new Path();
        // 設置 success 動畫的 path
        Path successPath = new Path();
        successPath.addCircle(mCenterX, mCenterY, radius, Path.Direction.CW);
        successPath.moveTo(mCenterX - radius * 0.5f, mCenterY - radius * 0.2f);
        successPath.lineTo(mCenterX - radius * 0.1f, mCenterY + radius * 0.4f);
        successPath.lineTo(mCenterX + radius * 0.6f, mCenterY - radius * 0.5f);
        mSuccessPathMeasure = new PathMeasure(successPath, false);
        mSuccessDstPath = new Path();
        // 設置 fail 動畫的 path
        Path failPath = new Path();
        failPath.addCircle(mCenterX, mCenterY, radius, Path.Direction.CW);
        failPath.moveTo(mCenterX - radius / 3, mCenterY - radius / 3);
        failPath.lineTo(mCenterX + radius / 3, mCenterY + radius / 3);
        failPath.moveTo(mCenterX + radius / 3, mCenterY - radius / 3);
        failPath.lineTo(mCenterX - radius / 3, mCenterY + radius / 3);
        mFailPathMeasure = new PathMeasure(failPath, false);
        mFailDstPath = new Path();
    }

    private void init() {
        // 取消硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        // 初始化畫筆
        mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBluePaint.setColor(Color.BLUE);
        mBluePaint.setStyle(Paint.Style.STROKE);
        mBluePaint.setStrokeCap(Paint.Cap.ROUND);
        mBluePaint.setStrokeWidth(10);

        mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRedPaint.setColor(Color.RED);
        mRedPaint.setStyle(Paint.Style.STROKE);
        mRedPaint.setStrokeCap(Paint.Cap.ROUND);
        mRedPaint.setStrokeWidth(10);

        // 初始化時, 動畫爲加載狀態
        curStatus = STATUS_LOADING;

        // 新建 Loading 動畫並 start
        mLoadingAnimator = ValueAnimator.ofFloat(0, 1);
        mLoadingAnimator.setDuration(2000);
        mLoadingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mProgress = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        mLoadingAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (curStatus == STATUS_SUCCESS) {
                    mSuccessAnimator.start();
                } else if (curStatus == STATUS_FAIL) {
                    mFailAnimator.start();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) { }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        });
        mLoadingAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mLoadingAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mLoadingAnimator.setRepeatMode(ValueAnimator.RESTART);
        mLoadingAnimator.start();
        // 新建 success 動畫
        mSuccessAnimator = ValueAnimator.ofFloat(0, 2);
        mSuccessAnimator.setDuration(1600);
        mSuccessAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mProgress = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        // 新建 fail 動畫
        mFailAnimator = ValueAnimator.ofFloat(0, 3);
        mFailAnimator.setDuration(2100);
        mFailAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mProgress = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
    }

    /**
     * 將動畫的狀態從 Loading 變爲 success 或 fail
     */
    public void setStatus(int status) {
        if (curStatus == STATUS_LOADING && status != STATUS_LOADING) {
            curStatus = status;
            mLoadingAnimator.end();
        }
    }

    private int mSuccessIndex = 1;
    private int mFailIndex = 1;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 在 Loading 狀態下 Canvas 會被旋轉, 需要在第一次進入時保存
        if (!hasCanvasSaved) {
            canvas.save();
            hasCanvasSaved = true;
        }
        // 判斷當前動畫的狀態並繪製相應動畫
        if (curStatus == STATUS_LOADING) {
            mDstPath.reset();
            float length = mPathMeasure.getLength();
            float stop = mProgress * length;
            float start = (float) (stop - (0.5 - Math.abs(mProgress - 0.5)) * length);
            mPathMeasure.getSegment(start, stop, mDstPath, true);
            // 旋轉畫布
            mCurRotate = (mCurRotate + 2) % 360;
            canvas.rotate(mCurRotate, mCenterX, mCenterY);
            canvas.drawPath(mDstPath, mBluePaint);
        } else if (curStatus == STATUS_SUCCESS) {
            if (!hasCanvasRestored) {
                canvas.restore();
                hasCanvasRestored = true;
            }
            if (mProgress < 1) {
                float stop = mSuccessPathMeasure.getLength() * mProgress;
                mSuccessPathMeasure.getSegment(0, stop, mSuccessDstPath, true);
            } else {
                if (mSuccessIndex == 1) {
                    mSuccessIndex = 2;
                    mSuccessPathMeasure.getSegment(0, mSuccessPathMeasure.getLength(),
                            mSuccessDstPath, true);
                    mSuccessPathMeasure.nextContour();
                }
                float stop = mSuccessPathMeasure.getLength() * (mProgress - 1);
                mSuccessPathMeasure.getSegment(0, stop, mSuccessDstPath, true);
            }
            canvas.drawPath(mSuccessDstPath, mBluePaint);
        } else if (curStatus == STATUS_FAIL) {
            if (!hasCanvasRestored) {
                canvas.restore();
                hasCanvasRestored = true;
            }
            if (mProgress < 1) {
                float stop = mFailPathMeasure.getLength() * mProgress;
                mFailPathMeasure.getSegment(0, stop, mFailDstPath, true);
            } else if (mProgress < 2) {
                if (mFailIndex == 1) {
                    mFailIndex = 2;
                    mFailPathMeasure.getSegment(0, mFailPathMeasure.getLength(),
                            mFailDstPath, true);
                    mFailPathMeasure.nextContour();
                }
                float stop = mFailPathMeasure.getLength() * (mProgress - 1);
                mFailPathMeasure.getSegment(0, stop, mFailDstPath, true);
            } else {
                if (mFailIndex == 2) {
                    mFailIndex = 3;
                    mFailPathMeasure.getSegment(0, mFailPathMeasure.getLength(),
                            mFailDstPath, true);
                    mFailPathMeasure.nextContour();
                }
                float stop = mFailPathMeasure.getLength() * (mProgress - 2);
                mFailPathMeasure.getSegment(0, stop, mFailDstPath, true);
            }
            canvas.drawPath(mFailDstPath, mRedPaint);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章