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

Github地址:TickView,一個精致的打鉤小動畫
https://github.com/ChengangFeng/TickView

1. 前言

最近在看輕芒雜志的時候,看到一個動畫很帶感很精致范抓;

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

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

qingmang.gif

看了后是不是感覺很精致,很帶感号俐?


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

my.gif

靜態(tài)圖

靜態(tài)圖

是不是模仿得有幾分相似泌豆,哈哈~,下面來看一下我實現(xiàn)的思路吧

2. 分析

這個動畫實現(xiàn)起來并不復雜吏饿,掌握幾個基本的自定義view的方法即可踪危。

實現(xiàn)的思路分為選中狀態(tài)未選中狀態(tài)

2.1 未選中的狀態(tài)

未選擇.png

未選中的狀態(tài)很簡單,需要繪制的有兩個圖形

  • 圓環(huán)

2.2 選中的狀態(tài)

繪制選中的動畫稍微復雜一點找岖,主要包括

  1. 繪制圓環(huán)進度條
    這個簡單陨倡,直接使用drawArc()即可實現(xiàn)

  2. 繪制向圓心收縮的動畫
    這個一開始的時候想用drawArc()加上設置畫筆的寬度strokeWidth來實現(xiàn)敛滋,不過改變的寬度是往外擴張的许布,所以這個想法果斷放棄。
    之后绎晃,我的想法是這樣的蜜唾,看下圖

    向圓心收縮的動畫分析

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

  1. 顯示勾出來
    關于這個√煤裙,我在網(wǎng)上搜了一波掩完,也沒有明確的指明怎么畫法才是標準的,所以這里可以隨意發(fā)揮硼砰,自己覺得好看就行且蓬。這里直接可以使用drawLine()可以一步搞定。
  2. 最后是圓環(huán)放大再回彈的效果
    放大回彈可以使用drawArc()题翰,配合改變畫筆的寬度來實現(xiàn)即可

3.具體實現(xiàn)

3.1 確定進度圓環(huán)和鉤的位置

經(jīng)過上面分析恶阴,無論是選中狀態(tài)還是未選中狀態(tài),進度圓環(huán)和鉤的位置是不變的豹障,所以我們先來確定圓環(huán)的位置和鉤的位置

@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);

    //設置打鉤的幾個點坐標(具體坐標點的位置不用怎么理會血公,自己定一個就好昵仅,沒有統(tǒng)一的標準)
    //畫一個√,需要確定3個坐標點的位置
    //所以這里我先用一個float數(shù)組來記錄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 定義變量岩饼,標記狀態(tài)

既然分選中狀態(tài)和未選中狀態(tài)荚虚,那個繪制過程中,就必須判斷當前究竟是繪制未選中的呢還是選中了的呢籍茧。

因此在這里版述,我定義了一個變量isChecked

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

//暴露外部接口,改變繪制狀態(tài)
public void setChecked(boolean checked) {
    if (this.isChecked != checked) {
        isChecked = checked;
        reset();
    }
}

3.3 繪制未選中狀態(tài)

繪制過程中那些畫筆就不詳細說了寞冯,一開始初始化畫筆最后繪制的時候調用即可

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

        //根據(jù)之前定好的鉤的坐標位置吮龄,進行繪制
        canvas.drawLines(mPoints, mPaintTick);
        return;
    }
}

3.4 繪制選中狀態(tài)

選中狀態(tài)是個動畫俭茧,因此我們這里需要調用postInvalidate()不斷進行重繪,直到動畫執(zhí)行完畢漓帚;另外母债,我這里用計數(shù)器的方式來控制繪制的進度。

3.4.1 繪制圓環(huán)進度條

繪制進度圓環(huán)這里尝抖,我們定義一個計數(shù)器ringCounter,峰值為360(也就是360度)毡们,每執(zhí)行一次onDraw()方法,我們對ringCounter進行自加昧辽,進而模擬進度衙熔。

最后記得調用postInvalidate()進行重繪

//計數(shù)器
private int ringCounter = 0;

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

這一步后效果圖如下

繪制圓環(huán)進度條.gif

3.4.2 繪制向圓心收縮的動畫

圓心收縮的動畫在圓環(huán)進度達到100%的時候才進行,同理咕痛,也采用計數(shù)器circleCounter的方法來控制繪制的時間和速度

//計數(shù)器
private int circleCounter = 0;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    ...
    //在圓環(huán)進度達到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

3.4.3 繪制鉤

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

繪制打鉤块仆,這里問題不大构蹬,因為在onMeasure()中已經(jīng)將鉤的三個坐標點已經(jīng)計算出來了,直接使用drawLine()即可畫出來悔据。

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

這一步后效果圖如下

繪制鉤后效果圖.gif

3.4.4 繪制放大再回彈的效果

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

因為這里要放大并且回彈怖亭,所以這里的計數(shù)器我設置成一個不為0的數(shù)值涎显,先設置成45(隨意,這不是標準)兴猩,然后沒重繪一次期吓,自減4個單位。

最后畫筆的寬度是關鍵的地方倾芝,畫筆的寬度根據(jù)scaleCounter的正負來決定是加還是減

//計數(shù)器
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);
    }
    //動畫執(zhí)行完畢,就補在需要重繪了
    if (scaleCounter != -45) {
        postInvalidate();
    }
}

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

最終的效果圖

3.5 暴露外部接口

為了靈活的可以控制繪制的狀態(tài)晨另,我們可以暴露一個接口給外部設置是否選中

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

/**
 *  重置潭千,并重繪
 */
private void reset() {
    //畫筆重置
    ...
    //計數(shù)器重置
    ringCounter = 0;
    circleCounter = 0;
    scaleCounter = 45;
    alphaCount = 0;
    ...
    invalidate();
}

3.6 添加點擊事件

控件到這里已經(jīng)基本做好了,但還不是特別的完善借尿。

想想checkbox刨晴,它不需要暴露外部接口也能通過點擊控件來實現(xiàn)選中還是取消選中,所以接下來要實現(xiàn)的就是為控件添加點擊事件

先定義一個接口OnCheckedChangeListener,實現(xiàn)監(jiān)聽此控件的監(jiān)聽事件

private OnCheckedChangeListener mOnCheckedChangeListener;

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

public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    this.mOnCheckedChangeListener = listener;
}

接下來垛玻,初始化控件的點擊事件

/**
 * 在構造函數(shù)中初始化
 */
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

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" />
    <!--動畫執(zhí)行的速度-->
    <attr name="rate">
        <enum name="slow" value="0"/>
        <enum name="normal" value="1"/>
        <enum name="fast" value="2"/>
    </attr>
</declare-styleable>

這里簡單說一下動畫執(zhí)行速度的配置割捅,這里我設置了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;

    //圓環(huán)進度增加的單位
    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的配置帚桩,獲取對應的枚舉,從而得到配好的動畫速度的一些參數(shù)

/**
 * 構造函數(shù)
 */
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账嚎,一個精致的打鉤小動畫
https://github.com/ChengangFeng/TickView

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市儡蔓,隨后出現(xiàn)的幾起案子郭蕉,更是在濱河造成了極大的恐慌,老刑警劉巖喂江,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件召锈,死亡現(xiàn)場離奇詭異,居然都是意外死亡获询,警方通過查閱死者的電腦和手機涨岁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吉嚣,“玉大人梢薪,你說我怎么就攤上這事〕⒍撸” “怎么了秉撇?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我琐馆,道長规阀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任瘦麸,我火速辦了婚禮姥敛,結果婚禮上,老公的妹妹穿的比我還像新娘瞎暑。我一直安慰自己彤敛,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布了赌。 她就那樣靜靜地躺著墨榄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勿她。 梳的紋絲不亂的頭發(fā)上袄秩,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音逢并,去河邊找鬼之剧。 笑死,一個胖子當著我的面吹牛砍聊,可吹牛的內容都是我干的背稼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼玻蝌,長吁一口氣:“原來是場噩夢啊……” “哼蟹肘!你這毒婦竟也來了?” 一聲冷哼從身側響起俯树,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帘腹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后许饿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阳欲,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年陋率,在試婚紗的時候發(fā)現(xiàn)自己被綠了球化。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡翘贮,死狀恐怖赊窥,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情狸页,我是刑警寧澤锨能,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布扯再,位于F島的核電站,受9級特大地震影響址遇,放射性物質發(fā)生泄漏熄阻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一倔约、第九天 我趴在偏房一處隱蔽的房頂上張望秃殉。 院中可真熱鬧,春花似錦浸剩、人聲如沸钾军。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吏恭。三九已至,卻和暖如春重罪,著一層夾襖步出監(jiān)牢的瞬間樱哼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工剿配, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留搅幅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓呼胚,卻偏偏與公主長得像茄唐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子砸讳,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容