Android自定義View:關於打鉤小動畫的思考重構

陳崗不姓陳發表於2017-10-29

Github地址:TickView,一個精緻的打鉤小動畫
github.com/ChengangFen…

先上效果圖,不然讀不下去了,right?

動圖

動圖.gif
動圖.gif

靜態圖

靜態圖
靜態圖


1. 回顧

【Android自定義View:一個精緻的打鉤小動畫】
上一篇文章,我們已經實現了基本上實現了控制元件的效果了,但是...但是...過了三四天後,仔細看回自己寫的程式碼,雖然思路還在,但是部分程式碼還是不能一下子的看得明白...

我的天,這得立馬重構啊~ 恰好,有個網友 ChangQin 模仿寫了一下這個控制元件,我看了後覺得我也可以這樣實現一下。

2. 深思

關於控制元件繪製的思路,可以去看看 上一篇文章,這裡就不再分析了。
這裡先來分析一下上一篇文章裡面,控制元件裡面的一些頑處,哪些地方需要改進。

就拿 繪製圓環進度 這一步來看

//計數器
private int ringCounter = 0;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!isChecked) {
        ...
        return;
    }
    //畫圓弧進度,每次繪製都自加12個單位,也就是圓弧又掃過了12度
    //這裡的12個單位先寫死,後面我們可以做一個配置來實現自定義
    ringCounter += 12;
    if (ringCounter >= 360) {
        ringCounter = 360;
    }
    canvas.drawArc(mRectF, 90, ringCounter, false, mPaintRing);
    ...
    //強制重繪
    postInvalidate();
}複製程式碼

這裡,我們定義了一個計數器ringCounter, 當繪製的時候,是根據12個單位進行自增到達360,從而模擬進度的變化。

仔細想想

  1. 通過改變自增的單位來控制動畫速度的變化,這很難調整得使自己滿意,此時我們可以想到,使動畫速度執行快慢的根本就是控制時間啊,如果可以用時間來控制動畫速度那得方便多了
  2. 動畫分為4步執行,如果每一步動畫都用手寫計數器來實現,那得定義4個成員變數或者更多,太多成員變數只會讓程式碼更加混亂
  3. 如果動畫要加上插值器,那手寫的計數器根本無法滿足
  4. 看到上面的分析,我無法接收了

3. 改改改

那麼怎麼去改善上面所說的問題呢,答案就是用自定義的屬性動畫來解決了,所以這篇文章主要的講的地方就是用屬性動畫來替換手寫的計數器,儘可能的保證程式碼邏輯的清晰,特別是onDraw()方法中的程式碼。

使用屬性動畫的一個好處就是,給定數值的範圍,它會幫你生成一堆你想要的數值,配合插值器還要意想不到的效果呢,下一面就一步一步針對動畫執行的部分進行重構

3.1 繪製圓環進度條

首先,使用自定義的ObjectAnimator來模擬進度

//ringProgress是自定義的屬性名稱,生成數值的範圍是0 - 360,就是一個圓的角度
ObjectAnimator mRingAnimator = ObjectAnimator.ofInt(this, "ringProgress", 0, 360);
//定義動畫執行的時間,很好的替代之前使用自增的單位來控制動畫執行的速度
mRingAnimator.setDuration(mRingAnimatorDuration);
//暫時不需要插值器
mRingAnimator.setInterpolator(null);複製程式碼

自定義屬性動畫,還需要配置相應的settergetter,因為在動畫執行的時候,會找相應的setter去改變相應的值。

private int getRingProgress() {
    return ringProgress;
}

private void setRingProgress(int ringProgress) {
    //動畫執行的時候,會呼叫setter
    //這裡我們可以將動畫生成的數值記錄下來,用變數存起來,在ondraw的時候用
    this.ringProgress = ringProgress;
    //記得重繪
    postInvalidate();
}複製程式碼

最後,在onDraw()中畫圖

//畫圓弧進度
canvas.drawArc(mRectF, 90, ringProgress, false, mPaintRing);複製程式碼

3.2 繪製向圓心收縮的動畫

同理,也是造一個屬性動畫

//這裡自定義的屬性是圓收縮的半徑
ObjectAnimator mCircleAnimator = ObjectAnimator.ofInt(this, "circleRadius", radius - 5, 0);
//加一個減速的插值器
mCircleAnimator.setInterpolator(new DecelerateInterpolator());
mCircleAnimator.setDuration(mCircleAnimatorDuration);複製程式碼

setter/getter也是類似就不說了

最後onDraw()中繪製

//畫背景
mPaintCircle.setColor(checkBaseColor);
canvas.drawCircle(centerX, centerY, ringProgress == 360 ? radius : 0, mPaintCircle);
//當進度圓環繪製好了,就畫收縮的圓
if (ringProgress == 360) {
    mPaintCircle.setColor(checkTickColor);
    canvas.drawCircle(centerX, centerY, circleRadius, mPaintCircle);
}複製程式碼

3.3 繪製鉤和放大再回彈的效果

這是兩個獨立的效果,這裡同時執行,我就合在一起說了

首先也是定義屬性動畫

//勾出來的透明漸變
ObjectAnimator mAlphaAnimator = ObjectAnimator.ofInt(this, "tickAlpha", 0, 255);
mAlphaAnimator.setDuration(200);
//最後的放大再回彈的動畫,改變畫筆的寬度來實現
//而畫筆的寬度,則是的變化範圍是
//首先從初始化寬度開始,再到初始化寬度的n倍,最後又回到初始化的寬度
ObjectAnimator mScaleAnimator = ObjectAnimator.ofFloat(this, "ringStrokeWidth", mPaintRing.getStrokeWidth(), mPaintRing.getStrokeWidth() * SCALE_TIMES, mPaintRing.getStrokeWidth() / SCALE_TIMES);
mScaleAnimator.setInterpolator(null);
mScaleAnimator.setDuration(mScaleAnimatorDuration);

//打鉤和放大回彈的動畫一起執行
AnimatorSet mAlphaScaleAnimatorSet = new AnimatorSet();
mAlphaScaleAnimatorSet.playTogether(mAlphaAnimator, mScaleAnimator);複製程式碼

getter/setter

private int getTickAlpha() {
    return 0;
}

private void setTickAlpha(int tickAlpha) {
    //設定透明度,可以不用變數來儲存了
    //直接將透明度的值設定到畫筆裡面即可
    mPaintTick.setAlpha(tickAlpha);
    postInvalidate();
}

private float getRingStrokeWidth() {
    return mPaintRing.getStrokeWidth();
}

private void setRingStrokeWidth(float strokeWidth) {
    //設定畫筆寬度,可以不用變數來儲存了
    //直接將畫筆寬度設定到畫筆裡面即可
    mPaintRing.setStrokeWidth(strokeWidth);
    postInvalidate();
}複製程式碼

最後,同理在onDraw()中繪製即可

if (circleRadius == 0) {
    canvas.drawLines(mPoints, mPaintTick);
    canvas.drawArc(mRectF, 0, 360, false, mPaintRing);
}複製程式碼

3.4 依次執行動畫

執行多個動畫,可以用到AnimatorSet,其中playTogether()是一起執行,playSequentially()是一個挨著一個,step by step執行。

mFinalAnimatorSet = new AnimatorSet();
mFinalAnimatorSet.playSequentially(mRingAnimator, mCircleAnimator, mAlphaScaleAnimatorSet);複製程式碼

最後在onDraw()中執行動畫

//這裡定義了一個識別符號,用於告訴程式,動畫每次只能執行一次
if (!isAnimationRunning) {
    isAnimationRunning = true;
    //執行動畫
    mFinalAnimatorSet.start();
}複製程式碼

3.5 每個方法最好能有單一的職責

如果將定義屬性動畫的方法放在onDraw()中,我個人感覺很亂,並且再仔細看看,這幾個屬性動畫是不需要動態變化的,為什麼不抽出來在一開始的時候就初始化呢?

so,我們將定義屬性動畫的程式碼抽出來,並且放到建構函式中初始化

public TickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    ...
    initAnimatorCounter();
}複製程式碼
/**
 * 用ObjectAnimator初始化一些計數器
 */
private void initAnimatorCounter() {
    //圓環進度
    ObjectAnimator mRingAnimator = ObjectAnimator.ofInt(this, "ringProgress", 0, 360);
    ...
    //收縮動畫
    ObjectAnimator mCircleAnimator = ObjectAnimator.ofInt(this, "circleRadius", radius - 5, 0);
    ...
    //勾出來的透明漸變
    ObjectAnimator mAlphaAnimator = ObjectAnimator.ofInt(this, "tickAlpha", 0, 255);
    ...
    //最後的放大再回彈的動畫,改變畫筆的寬度來實現
    ObjectAnimator mScaleAnimator = ObjectAnimator.ofFloat(this, "ringStrokeWidth", mPaintRing.getStrokeWidth(), mPaintRing.getStrokeWidth() * SCALE_TIMES, mPaintRing.getStrokeWidth() / SCALE_TIMES);
    ...

    //打鉤和放大回彈的動畫一起執行
    AnimatorSet mAlphaScaleAnimatorSet = new AnimatorSet();
    mAlphaScaleAnimatorSet.playTogether(mAlphaAnimator, mScaleAnimator);

    mFinalAnimatorSet = new AnimatorSet();
    mFinalAnimatorSet.playSequentially(mRingAnimator, mCircleAnimator, mAlphaScaleAnimatorSet);
}複製程式碼

最後,onDraw()方法中,只負責簡單的繪製,什麼都不管

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!isChecked) {
        canvas.drawArc(mRectF, 90, 360, false, mPaintRing);
        canvas.drawLines(mPoints, mPaintTick);
        return;
    }
    //畫圓弧進度
    canvas.drawArc(mRectF, 90, ringProgress, false, mPaintRing);
    //畫黃色的背景
    mPaintCircle.setColor(checkBaseColor);
    canvas.drawCircle(centerX, centerY, ringProgress == 360 ? radius : 0, mPaintCircle);
    //畫收縮的白色圓
    if (ringProgress == 360) {
        mPaintCircle.setColor(checkTickColor);
        canvas.drawCircle(centerX, centerY, circleRadius, mPaintCircle);
    }
    //畫勾,以及放大收縮的動畫
    if (circleRadius == 0) {
        canvas.drawLines(mPoints, mPaintTick);
        canvas.drawArc(mRectF, 0, 360, false, mPaintRing);
    }
    //ObjectAnimator動畫替換計數器
    if (!isAnimationRunning) {
        isAnimationRunning = true;
        mFinalAnimatorSet.start();
    }
}複製程式碼

最終效果是一樣的,程式碼邏輯一目瞭然

最終效果.gif
最終效果.gif

所以,個人覺得,在開發中,定時review一下自己的程式碼,無論對自己,還是對以後維護,是很有幫助的。

That ' s all~
感謝大家閱讀,最後再放一下專案的github地址

Github地址:TickView,一個精緻的打鉤小動畫
github.com/ChengangFen…

相關文章