Android自定義View:一個精緻的打鉤小動畫

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

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

1. 前言

最近在看輕芒雜誌的時候,看到一個動畫很帶感很精緻;

恰好這段時間也在看【HenCoder】的自定義view教程(裡面寫得非常非常詳細,也有相應的習題等等),所以就趁熱打鐵,熟悉一下學習的知識。

國際慣例,先上輕芒雜誌標記已讀的動畫

qingmang.gif
qingmang.gif

看了後是不是感覺很精緻,很帶感?


那下面來看一下我自己模仿的效果

my.gif
my.gif

是不是模仿得有幾分相似,哈哈~,下面來看一下我實現的思路吧

2. 分析

這個動畫實現起來並不複雜,掌握幾個基本的自定義view的方法即可。

實現的思路分為選中狀態未選中狀態

2.1 未選中的狀態

未選擇.png
未選擇.png

未選中的狀態很簡單,需要繪製的有兩個圖形

  • 圓環

2.2 選中的狀態

繪製選中的動畫稍微複雜一點,主要包括

  1. 繪製圓環進度條
    這個簡單,直接使用drawArc()即可實現

  2. 繪製向圓心收縮的動畫
    這個一開始的時候想用drawArc()加上設定畫筆的寬度strokeWidth來實現,不過改變的寬度是往外擴張的,所以這個想法果斷放棄。
    之後,我的想法是這樣的,看下圖

    向圓心收縮的動畫分析
    向圓心收縮的動畫分析

    我就打算先繪製一個黃色的背景,然後在這個圖層上面繪製一個白色的圓,半徑不斷的縮小,直至為0,這就反過來得到了一個向中心收縮的動畫,這可以叫逆轉思維吧,最近看的一本書裡面說到有時候反過來思考也許會有不一樣的效果。

  1. 顯示勾出來
    關於這個√,我在網上搜了一波,也沒有明確的指明怎麼畫法才是標準的,所以這裡可以隨意發揮,自己覺得好看就行。這裡直接可以使用drawLine()可以一步搞定。
  2. 最後是圓環放大再回彈的效果
    放大回彈可以使用drawArc(),配合改變畫筆的寬度來實現即可

3.具體實現

3.1 確定進度圓環和鉤的位置

經過上面分析,無論是選中狀態還是未選中狀態,進度圓環和鉤的位置是不變的,所以我們先來確定圓環的位置和鉤的位置

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
    //設定圓圈的外切矩形,radius是圓的半徑,centerX,centerY是控制元件中心的座標
    mRectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);

    //設定打鉤的幾個點座標(具體座標點的位置不用怎麼理會,自己定一個就好,沒有統一的標準)
    //畫一個√,需要確定3個座標點的位置
    //所以這裡我先用一個float陣列來記錄3個座標點的位置,
    //最後在onDraw()的時候使用canvas.drawLines(mPoints, mPaintTick)來畫出來
    //其中這裡mPoint[0]~mPoint[3]是確定第一條線 \ 的兩個座標點位置
    //mPoint[4]~mPoint[7]是確定第二條線 / 的兩個座標點位置

    mPoints[0] = centerX - tickRadius + tickRadiusOffset;
    mPoints[1] = (float) centerY;
    mPoints[2] = centerX - tickRadius / 2 + tickRadiusOffset;
    mPoints[3] = centerY + tickRadius / 2;
    mPoints[4] = centerX - tickRadius / 2 + tickRadiusOffset;
    mPoints[5] = centerY + tickRadius / 2;
    mPoints[6] = centerX + tickRadius * 2 / 4 + tickRadiusOffset;
    mPoints[7] = centerY - tickRadius * 2 / 4;
}複製程式碼

3.2 定義變數,標記狀態

既然分選中狀態和未選中狀態,那個繪製過程中,就必須判斷當前究竟是繪製未選中的呢還是選中了的呢。

因此在這裡,我定義了一個變數isChecked

//是否被點亮
private boolean isChecked = false;

//暴露外部介面,改變繪製狀態
public void setChecked(boolean checked) {
    if (this.isChecked != checked) {
        isChecked = checked;
        reset();
    }
}複製程式碼

3.3 繪製未選中狀態

繪製過程中那些畫筆就不詳細說了,一開始初始化畫筆最後繪製的時候呼叫即可

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!isChecked) {
        //繪製圓環,mRectF就是之前確定的外切矩形
        //因為是靜態的,所以設定掃過的角度為360度
        canvas.drawArc(mRectF, 90, 360, false, mPaintRing);

        //根據之前定好的鉤的座標位置,進行繪製
        canvas.drawLines(mPoints, mPaintTick);
        return;
    }
}複製程式碼

3.4 繪製選中狀態

選中狀態是個動畫,因此我們這裡需要呼叫postInvalidate()不斷進行重繪,直到動畫執行完畢;另外,我這裡用計數器的方式來控制繪製的進度。

3.4.1 繪製圓環進度條

繪製進度圓環這裡,我們定義一個計數器ringCounter,峰值為360(也就是360度),每執行一次onDraw()方法,我們對ringCounter進行自加,進而模擬進度。

最後記得呼叫postInvalidate()進行重繪

//計數器
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();
}複製程式碼

這一步後效果圖如下

繪製圓環進度條.gif
繪製圓環進度條.gif

3.4.2 繪製向圓心收縮的動畫

圓心收縮的動畫在圓環進度達到100%的時候才進行,同理,也採用計數器circleCounter的方法來控制繪製的時間和速度

//計數器
private int circleCounter = 0;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    ...
    //在圓環進度達到100%的時候才開始繪製
    if (ringCounter == 360) {
        //先繪製背景的圓
        mPaintCircle.setColor(checkBaseColor);
        canvas.drawCircle(centerX, centerY, radius, mPaintCircle);
        //然後在背景圓的圖層上,再繪製白色的圓(半徑不斷縮小)
        //半徑不斷縮小,背景就不斷露出來,達到向中心收縮的效果
        mPaintCircle.setColor(checkTickColor);
        //收縮的單位先試著設定為6,後面可以進行自己自定義
        circleCounter += 6;
        canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle);
    }
    //必須重繪
    postInvalidate();
}複製程式碼

這一步後效果圖如下

繪製向圓心收縮的動畫.gif
繪製向圓心收縮的動畫.gif

3.4.3 繪製鉤

當白色的圓半徑收縮到0後,就該繪製打鉤了。

繪製打鉤,這裡問題不大,因為在onMeasure()中已經將鉤的三個座標點已經計算出來了,直接使用drawLine()即可畫出來。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    ...
    canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle);
    //當白色的圓半徑收縮到0後,
    //也就是計數器circleCounter大於背景圓的半徑的時候,就該將鉤√顯示出來了
    //這裡加40是為了加一個延遲時間,不那麼倉促的將鉤顯示出來
    if (circleCounter >= radius + 40) {
        //顯示打鉤(外加一個透明的漸變)
        alphaCount += 20;
        if (alphaCount >= 255) alphaCount = 255;
        mPaintTick.setAlpha(alphaCount);
        //最後就將之前在onMeasure中計算好的座標傳進去,繪製鉤出來
        canvas.drawLines(mPoints, mPaintTick);
    }
    postInvalidate();
}複製程式碼

這一步後效果圖如下

繪製鉤後效果圖.gif
繪製鉤後效果圖.gif

3.4.4 繪製放大再回彈的效果

放大再回彈的效果,開始的時機應該也是收縮動畫結束後開始,也就是說跟打鉤的動畫同時進行

因為這裡要放大並且回彈,所以這裡的計數器我設定成一個不為0的數值,先設定成45(隨意,這不是標準),然後沒重繪一次,自減4個單位。

最後畫筆的寬度是關鍵的地方,畫筆的寬度根據scaleCounter的正負來決定是加還是減

//計數器
private int scaleCounter = 45;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    ...
    if (circleCounter >= radius + 40) {
        //顯示打鉤
       ...
        //顯示放大並回彈的效果
        scaleCounter -= 4;
        if (scaleCounter <= -45) {
            scaleCounter = -45;
        }
        //放大回彈,主要看畫筆的寬度
        float strokeWith = mPaintRing.getStrokeWidth() + 
            (scaleCounter > 0 ? dp2px(mContext, 1) : -dp2px(mContext, 1));
        mPaintRing.setStrokeWidth(strokeWith);
        canvas.drawArc(mRectF, 90, 360, false, mPaintRing);
    }
    //動畫執行完畢,就補在需要重繪了
    if (scaleCounter != -45) {
        postInvalidate();
    }
}複製程式碼

完成最後一步的最終效果圖

最終的效果圖
最終的效果圖

3.5 暴露外部介面

為了靈活的可以控制繪製的狀態,我們可以暴露一個介面給外部設定是否選中

/**
 *  是否選中
 */
public void setChecked(boolean checked) {
    if (this.isChecked != checked) {
        isChecked = checked;
        reset();
    }
}

/**
 *  重置,並重繪
 */
private void reset() {
    //畫筆重置
    ...
    //計數器重置
    ringCounter = 0;
    circleCounter = 0;
    scaleCounter = 45;
    alphaCount = 0;
    ...
    invalidate();
}複製程式碼

3.6 新增點選事件

控制元件到這裡已經基本做好了,但還不是特別的完善。

想想checkbox,它不需要暴露外部介面也能通過點選控制元件來實現選中還是取消選中,所以接下來要實現的就是為控制元件新增點選事件

先定義一個介面OnCheckedChangeListener,實現監聽此控制元件的監聽事件

private OnCheckedChangeListener mOnCheckedChangeListener;

public interface OnCheckedChangeListener {
    void onCheckedChanged(TickView tickView, boolean isCheck);
}

public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    this.mOnCheckedChangeListener = listener;
}複製程式碼

接下來,初始化控制元件的點選事件

/**
 * 在建構函式中初始化
 */
public TickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    ...
    setUpEvent();
}

/**
 * 初始化點選事件
 */
private void setUpEvent() {
    this.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            isChecked = !isChecked;
            reset();
            if (mOnCheckedChangeListener != null) {
                //此處回撥
                mOnCheckedChangeListener.onCheckedChanged((TickView) view, isChecked);
            }
        }
    });
}複製程式碼

看看效果圖

新增點選事件.gif
新增點選事件.gif

3.7 自定義配置項

<declare-styleable name="TickView">
    <!--沒有選中的基調顏色-->
    <attr name="uncheck_base_color" format="color" />
    <!--選中後的基調顏色-->
    <attr name="check_base_color" format="color" />
    <!--選中後鉤的顏色-->
    <attr name="check_tick_color" format="color" />
    <!--圓的半徑-->
    <attr name="radius" format="dimension" />
    <!--動畫執行的速度-->
    <attr name="rate">
        <enum name="slow" value="0"/>
        <enum name="normal" value="1"/>
        <enum name="fast" value="2"/>
    </attr>
</declare-styleable>複製程式碼

這裡簡單說一下動畫執行速度的配置,這裡我設定了3檔速度,我用列舉定義了三個速度的配置項

enum TickRateEnum {

    //低速
    SLOW(6, 4, 2),
    //正常速度
    NORMAL(12, 6, 4),
    //高速
    FAST(20, 14, 8);

    public static final int RATE_MODE_SLOW = 0;
    public static final int RATE_MODE_NORMAL = 1;
    public static final int RATE_MODE_FAST = 2;

    //圓環進度增加的單位
    private int ringCounterUnit;
    //圓圈收縮的單位
    private int circleCounterUnit;
    //圓圈最後放大收縮的單位
    private int scaleCounterUnit;

    public static TickRateEnum getRateEnum(int rateMode) {
        TickRateEnum tickRateEnum;
        switch (rateMode) {
            case RATE_MODE_SLOW:
                tickRateEnum = TickRateEnum.SLOW;
                break;
            case RATE_MODE_NORMAL:
                tickRateEnum = TickRateEnum.NORMAL;
                break;
            case RATE_MODE_FAST:
                tickRateEnum = TickRateEnum.FAST;
                break;
            default:
                tickRateEnum = TickRateEnum.NORMAL;
                break;
        }
        return tickRateEnum;
    }

    ...
}複製程式碼

獲取xml的配置,獲取對應的列舉,從而得到配好的動畫速度的一些引數

/**
 * 建構函式
 */
public TickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    ...
    initAttrs(attrs);
}

/**
 * 獲取自定義配置
 */
private void initAttrs(AttributeSet attrs) {
    TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.TickView);
    ...
    //獲取配置的動畫速度
    int rateMode = typedArray.getInt(R.styleable.TickView_rate, TickRateEnum.RATE_MODE_NORMAL);
    mTickRateEnum = TickRateEnum.getRateEnum(rateMode);
    typedArray.recycle();
}複製程式碼

最終成果圖

最終成果圖
最終成果圖

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

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

相關文章