1. 程式人生 > >自定義View實戰--實現一個清新美觀的載入按鈕

自定義View實戰--實現一個清新美觀的載入按鈕

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

在 Dribble 上偶然看到了一組互動如下:
這裡寫圖片描述

當時在心裡問自己能不能做,答案肯定是能做的,不過我比較懶,覺得中間那個伸縮變化要編寫很多程式碼,所以懶得理。後來,為了不讓自己那麼浮躁,也為了鍛鍊自己的耐心程度,還是堅持實現它了。這個過程,覺得自己還是有所收穫,把握了一些想當然的細節,輸理了對於自定義 View 的流程。

我將這個自定義 View,起了一個名字叫做 LoadButton。

這篇文章涉及到的知識點有如下:

1. 自定義 View 時的基本流程,包含 attrs.xml 中屬性的編寫,構造方法中屬性的獲取,onMeasure() 中尺寸的測量。onDraw() 中介面的實現。

2. 可以讓 Android 初學者再次感受一次回撥機制的美妙。

3. 屬性動畫的基本使用。

第一步,先確定尺寸

先觀察 LoadView 的形態。
這裡寫圖片描述

上面的顯示的是兩種形狀,一個是圓角矩形,另外一個就是圓。兩個形態尺寸區別是,高相同,寬度不一致。

我們再進一步分析形態 1。
這裡寫圖片描述

形態 1 可以看成是左右兩個半圓和中間一個矩形。再回顧下示例圖片中的動畫表現。
這裡寫圖片描述

圓角矩形最終變成了一個圓。我們可以用線框圖來漸進表現它。
這裡寫圖片描述

當進行動畫時,中間的矩形部分不停地縮小,當它縮小為 0 時,形態 1 就轉變成了形態 2。

上面的能夠說明什麼呢?說明 LoadButton 由 3 個部分組成,左右的半圓和中間的矩形,即使是形態 2 也可以看做是左右半圓和中間寬度為 0 的矩形組成。
這裡寫圖片描述

細化尺寸

我們進一步討論尺寸相關的情況。

我們知道對於普通開發者而言,自定義一個 View 測量尺寸的時候我們通常要關注的測量模式是 MeasureSpec.EXACTLY 和 MeasureSpec.AT_MOST 兩種。要了解更多詳細的資訊可以閱讀我寫的這篇博文《長談:關於 View Measure 測量機制,讓我一次把話說完》。接下來,我們詳細討論一下這兩種情況。

MeasureSpec.EXACTLY

當一個 View 的 layout_width 或者 layout_height 的取值為 match_parent 或 30dp 這樣具體的數值時,這就表明它的測量模式是 MeasureSpec.EXACTLY。它已經獲得了精確的數值了,按照常理我們是不應該再去幹涉它,parent 給出的建議尺寸是什麼,我們就把尺寸設定成什麼,但是結合開發的實際情況來看,我們有一個底線,為了保證 LoadView 的完整性,也就是再差的情況下,parent 給出來的建議尺寸也不能小於形態 2。否則如下圖情況就不是我們想要的了
這裡寫圖片描述

MeasureSpec.AT_MOST

當一個 View 的 layout_width 或者 layout_height 的取值為 wrap_content 時,它的測量模式就是 MeasureSpec.AT_MOST,這個時候我們需要自己根據內容計算尺寸。而 LoadButton 的內容是什麼呢?它的內容有 text 還有 載入成功或者載入失敗的圖片。因為圖片大小在形態 2 中的圓形內可以確認。所以問題的關鍵就在於 LoadButton 文字內容寬高的尺寸測量。
這裡寫圖片描述

text 內容自然是居中顯示,然後它距離中間的 rect 上下左右間距也要考慮。這個時候的 rect 尺寸就是相對應的文字尺寸加上相對應方向上的 padding 值,這些 padding 值通過在 attrs.xml 中自定義屬性然後在佈局檔案中賦予。

最後整體 LoadButton 尺寸自然是中間 rect 加上左右兩個半圓的半徑,但是這還不是最終的尺寸,最終的尺寸還是要和 parent 給的建議尺寸比較,不能大於它。

上面分析了尺寸測量相關,所以順著思路進行的話,編碼也只是水到渠成的事情了。

public class LoadButton extends View {

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);

    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //用於儲存最終尺寸
    int resultW = widthSize;
    int resultH = heightSize;

    // contentW contentH 用於確定中間矩形的尺寸
    int contentW = 0;
    int contentH = 0;

    if ( widthMode == MeasureSpec.AT_MOST ) {
        mTextWidth = (int) mTextPaint.measureText(mText);
        contentW += mTextWidth + mLeftRightPadding * 2 + mRadiu * 2;

        resultW = contentW < widthSize ? contentW : widthSize;
    }

    if ( heightMode == MeasureSpec.AT_MOST ) {
        contentH += mTopBottomPadding * 2 + mTextSize;
        resultH = contentH < heightSize ? contentH : heightSize;
    }

    resultW = resultW < 2 * mRadiu ? 2 * mRadiu : resultW;
    resultH = resultH < 2 * mRadiu ? 2 * mRadiu : resultH;

    // 修整圓形的半徑
    mRadiu = resultH / 2;
    // 記錄中間矩形的寬度值
    rectWidth = resultW - 2 * mRadiu;
    setMeasuredDimension(resultW,resultH);

    Log.d(TAG,"onMeasure: w:"+resultW+" h:"+resultH);
}

}

第二步,繪製

測量是在 onMeasure() 方法中進行,而繪製就是在 onDraw() 方法中進行的,這是 Android 開發者都知道的事情。所以這一節的重點在於 onDraw() 這個方法。
為了不給讀者造成困擾,我先張貼自定的屬性,及在構造方法中獲取屬性值的程式碼。其它的細節應該看名字就大概知道了。
attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LoadButton">
        <attr name="android:text" />
        <attr name="android:textSize" />
        <attr name="stroke_color" format="color|reference" />
        <attr name="content_color" format="color|reference" />
        <attr name="radiu" format="dimension|reference" />
        <attr name="rectwidth" format="dimension|reference" />
        <attr name="contentPaddingLR" format="dimension|reference" />
        <attr name="contentPaddingTB" format="dimension|reference" />
        <attr name="progressedWidth" format="dimension|reference" />
        <attr name="backColor" format="color|reference" />
        <attr name="progressColor" format="color|reference" />
        <attr name="progressSecondColor" format="color|reference" />
        <attr name="loadSuccessDrawable" format="reference" />
        <attr name="loadErrorDrawable" format="reference" />
        <attr name="loadPauseDrawable" format="reference" />
    </declare-styleable>
</resources>

然後在 LoadButton 的構造方法中獲取這些值。

public LoadButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mDefaultRadiu = 40;
        mDefaultTextSize = 24;
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.LoadButton);
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.LoadButton_android_textSize,
                mDefaultTextSize);
        mStrokeColor = typedArray.getColor(R.styleable.LoadButton_stroke_color, Color.RED);
        mTextColor = typedArray.getColor(R.styleable.LoadButton_content_color, Color.WHITE);
        mText = typedArray.getString(R.styleable.LoadButton_android_text);
        mRadiu = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_radiu,mDefaultRadiu);
        mTopBottomPadding = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_contentPaddingTB,10);
        mLeftRightPadding = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_contentPaddingLR,10);
        mBackgroundColor = typedArray.getColor(R.styleable.LoadButton_backColor,Color.WHITE);
        mProgressColor = typedArray.getColor(R.styleable.LoadButton_progressColor,Color.WHITE);
        mProgressSecondColor = typedArray.getColor(R.styleable.LoadButton_progressSecondColor,Color.parseColor("#c3c3c3"));
        mProgressWidth = typedArray.getDimensionPixelOffset(R.styleable.LoadButton_progressedWidth,2);

        mSuccessedDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadSuccessDrawable);
        mErrorDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadErrorDrawable);
        mPauseDrawable = typedArray.getDrawable(R.styleable.LoadButton_loadPauseDrawable);
        typedArray.recycle();

        ......

}

形態 1 的繪製,藉助於 Path 的力量

Android 繪製圖形離不開 Canvas,Canvas 可以直接繪製 直線、矩形、圓、橢圓,但是 LoadButton 的形態 1 怎麼繪製呢?它是一個不規則的閉合圖形,直接用 Canvas 的話肯定不行,所以得藉助另外一個類 Path,Path 中文譯做路徑,可以專門處理這種情況,而且可以處理比這複雜的情況,具體情況請讀者們自己查閱相應資料與教程。

我們再來觀察 形態 1 到形態 2 的轉變過程。
這裡寫圖片描述

這是個中間矩形從初始值變為 0 的過程,我們用 rectWidth 表示這個矩形的寬度值,因為在 onDraw() 方法中,LoadButton 尺寸確定,所以我們很容易得到它的中心點,所以我們可以中心點座標為參考座標,然後以 rectWidth 為變數建立一個 path,這個 path 實現了 LoadButton 的輪廓。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int cx = getWidth() / 2;
    int cy = getHeight() / 2;

    drawPath(canvas,cx,cy);

    .....

}


private void drawPath(Canvas canvas,int cx,int cy) {
    if (mPath == null) {
        mPath = new Path();
    }

    mPath.reset();

    left = cx - rectWidth / 2 - mRadiu;
    top = 0;
    right = cx + rectWidth / 2 + mRadiu;
    bottom = getHeight();

    leftRect.set(left,top,left + mRadiu * 2,bottom);
    rightRect.set(right - mRadiu * 2,top,right,bottom);
    contentRect.set(cx-rectWidth/2,top,cx + rectWidth/2,bottom);
    //path 起始位置
    mPath.moveTo(cx - rectWidth /2,bottom);
    // 左邊半圓
    mPath.arcTo(leftRect,
            90.0f,180f);
    //連線到右邊半圓
    mPath.lineTo(cx + rectWidth/2,top);
    // 右邊半圓
    mPath.arcTo(rightRect,
            270.0f,180f);
    // path 閉合
    mPath.close();

    // 以填充的方向將圖形填充為指定的背景色
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mBackgroundColor);
    canvas.drawPath(mPath,mPaint);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(mStrokeColor);
}

以 rectWidth 為變數建立 path 的好處時,當從形態 1 到 形態 2 轉變的過程,肯定是 rectWidth 數值變化的過程,而對於其它數值是不變的,所以重繪的時候 LoadButton 能夠很輕鬆地處理這種情況。

我們到這一步的時候已經能夠準確地繪製了 LoadButton 的輪廓。現在需要精確地繪製它的內容,只有這樣才是完整的 LoadButton。

我們先需要給 LoadButton 定義一些狀態。

LoadButton 的狀態

enum State {
    INITIAL,// 初始狀態
    FOLDING,// 正在伸縮
    LOADING, // 正在載入
    ERROR,// 載入失敗
    SUCCESSED,// 載入成功
    PAUSED // 載入暫停
}

這裡寫圖片描述

它們的狀態轉換如下:
這裡寫圖片描述

LoadButton 的狀態轉換由使用者點選按鈕觸發。所以 LoadButton 需要在內部設定一個 OnClickListenner。
1. 當在 Initial 狀態下點選時,它會轉換到 Folding 狀態下。
2. Foding 狀態結束後,由形態 1 轉變成形態 2。自然就進入了 Loading 狀態。
3. Loading 狀態有 3 個走向,載入成功後,使用者通過相應 API 設定狀態為 Successed。載入失敗後,使用者可以設定狀態為 Error。如果在 Loading 狀態下點選按鈕,會進入 Paused 狀態。
4. 在 Paused 狀態下點選按鈕,LoadButton 重新進入 Loading 狀態。
5. 在 Successed 或者 Error 狀態下點選按鈕,將通過回撥物件,通知呼叫者點選事件的發生。

我們在 LoadButton 的構造方法中設定這樣的內部的 OnClickListenner。

public LoadButton(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    ......

    isUnfold = true;

    mListenner = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if ( mCurrentState == State.FODDING) {
                return;
            }

            if ( mCurrentState == State.INITIAL ) {
                if ( isUnfold ) {
                    shringk();
                }
            } else if ( mCurrentState == State.ERROR) {

                if (mLoadListenner != null ) {
                    mLoadListenner.onClick(false);
                }


            } else if ( mCurrentState == State.SUCCESSED ) {
                if (mLoadListenner != null ) {
                    mLoadListenner.onClick(true);
                }
            } else if ( mCurrentState == State.PAUSED) {
                if (mLoadListenner != null ) {
                    mLoadListenner.needLoading();
                    load();
                }
            } else if ( mCurrentState == State.LOADDING) {
                mCurrentState = State.PAUSED;
                cancelAnimation();
                invaidateSelft();
            }

        }
    };

    setOnClickListener(mListenner);

    mCurrentState = State.INITIAL;


    ......
}

狀態的繪製

Initial 狀態下其實就是中間一個 text 文字居中顯示,相關程式碼如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    ......

    int textDescent = (int) mTextPaint.getFontMetrics().descent;
    int textAscent = (int) mTextPaint.getFontMetrics().ascent;
    int delta = Math.abs(textAscent) - textDescent;

    if ( mCurrentState == State.INITIAL) {

        canvas.drawText(mText,cx,cy + delta / 2,mTextPaint);

    } 

    .....
}

Folding 狀態其實就是不顯示文字的 Inital 狀態,不同的還有它的 rectwidth 每次重繪時會變小,最終會由 Initial 的形態 1 過渡到 Loading 狀態下的形態 2。在 Initial 狀態下點選按鈕會呼叫一個動畫,這個動畫用於展示形態 1 到形態 2 的過程。

if ( mCurrentState == State.INITIAL ) {
    if ( isUnfold ) {
        shringk();
    }
}


public void shringk() {
    if (shrinkAnim == null) {
        shrinkAnim = ObjectAnimator.ofInt(this,"rectWidth", rectWidth,0);
    }
    shrinkAnim.addListener(this);

    shrinkAnim.setDuration(500);
    shrinkAnim.start();
    mCurrentState = State.FOLDING;
}

public void setRectWidth (int width) {
    rectWidth = width;
    invaidateSelft();
}

private void invaidateSelft() {
    if (Looper.myLooper() == Looper.getMainLooper()) {
        invalidate();
    } else {
        postInvalidate();
    }
}

這裡是一個典型的屬性動畫應用場景,通過不斷改變屬性 rectWidth 的值來進行重繪,而對於繪製這一方面,文章前面部分有說過 LoadButton 通過以中心座標為參考,以 mRectWidth 為變數建立了一個 Path 來繪製輪廓。

另外,大家可以注意到,shrinkAnim 有一個監聽器,我設定為了 LoadButton 本身。

public class LoadButton extends View implements Animator.AnimatorListener {
@Override
    public void onAnimationStart(Animator animation) {

    }

    @Override
    public void onAnimationEnd(Animator animation) {
        isUnfold = false;
        load();
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }   

}

在收縮動畫結束的時候,我呼叫了 load() 方法用來將狀態設定為 Loading,並進行載入動畫。

我們先看看 Loading 狀態下的繪製,它是形態 2 ,也就是在一個圓形內有一個正在載入無限迴圈的動畫。思路也很簡單,用進度條的背景色畫一個圓圈,然後用進度條的前景色繪製相應角度的弧,並且這個弧的半徑和進度條的半徑一樣。

if ( mCurrentState == State.LOADING) {

    if ( progressRect == null ) {
        progressRect = new RectF();
    }
    progressRect.set(cx - circleR,cy - circleR,cx + circleR,cy + circleR);

    mPaint.setColor(mProgressSecondColor);
    //先繪製背景圓
    canvas.drawCircle(cx,cy,circleR,mPaint);
    mPaint.setColor(mProgressColor);
    Log.d(TAG,"onDraw() pro:"+progressReverse+" swpeep:"+circleSweep);
    if ( circleSweep != 360 ) {
        mProgressStartAngel = progressReverse ? 270 : (int) (270 + circleSweep);
        //繪製弧線
        canvas.drawArc(progressRect
        ,mProgressStartAngel,progressReverse ? circleSweep : (int) (360 - circleSweep),
                false,mPaint);
    }

    mPaint.setColor(mBackgroundColor);
}

上面有兩個關鍵的變數 progressReverse 和 circleSweep。progressReverse 用來表示動畫是否需要翻轉,circleSweep 表示每次繪製的時候從起始角度掃描的角度。
正常情況下,起始角度是 270 度不變,如果動畫翻轉時,它是 270 + circleSweep 的值,具體為什麼這樣做,大家可以觀看之前的影象來思考一下。
載入的動畫自然也是屬性動畫控制的,這個動畫讓 circleSweep 從 0 到 360 之間不停地變化。並且在每次迴圈的時候,將 progressReverse 變數置反。

public void load() {
    if (loadAnimator == null) {
        loadAnimator = ObjectAnimator.ofFloat(this,"circleSweep",0,360);
    }

    loadAnimator.setDuration(1000);
    loadAnimator.setRepeatMode(ValueAnimator.RESTART);
    loadAnimator.setRepeatCount(ValueAnimator.INFINITE);

    loadAnimator.removeAllListeners();

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

        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {
//                Log.d(TAG,"onAnimationRepeat:"+progressReverse);
            progressReverse = !progressReverse;
        }
    });
    loadAnimator.start();
    mCurrentState = State.LOADING;
}

Paused 狀態是當 LoadButton 在 Loading 狀態下,使用者點選了按鈕,這個時候按鈕會顯示一個暫停圖示。

if ( mCurrentState == State.LOADING) {
    mCurrentState = State.PAUSED;
    cancelAnimation();
    invaidateSelft();
}

至於顯示方面,非常簡單就是給一個 drawable 設定好 bound 範圍然後顯示。稍後我會給出程式碼。

Successed 狀態和 Error 狀態實現過程基本上是一致的。但是它們被點選的時候,需要通知點選者。所以我們需要定義一個回撥介面。

if ( mCurrentState == State.ERROR) {

    if (mLoadListenner != null ) {
        mLoadListenner.onClick(false);
    }


} else if ( mCurrentState == State.SUCCESSED ) {
    if (mLoadListenner != null ) {
        mLoadListenner.onClick(true);
    }
} else if ( mCurrentState == State.PAUSED) {
    if (mLoadListenner != null ) {
        mLoadListenner.needLoading();
        load();
    }
}else if ( mCurrentState == State.PAUSED) {
    if (mLoadListenner != null ) {
        mLoadListenner.needLoading();
        load();
    }
} 

public interface LoadListenner {

    void onClick(boolean isSuccessed);

    void needLoading();
}

LoadListenner.onClick() 方法中的引數,isSuccessed 為真告訴點選者載入成功了的資訊。否則提示載入失敗。needLoading() 方法用來告訴點選者當在 Paused 狀態下點選按鈕時,呼叫者應該重新載入了。

它們的顯示程式碼如下:

if ( mCurrentState == State.ERROR) {
    mErrorDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
    mErrorDrawable.draw(canvas);
} else if (mCurrentState == State.SUCCESSED) {
    mSuccessedDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
    mSuccessedDrawable.draw(canvas);
} else if (mCurrentState == State.PAUSED) {
    mPauseDrawable.setBounds(cx - circleR,cy - circleR,cx + circleR,cy + circleR);
    mPauseDrawable.draw(canvas);
}

另外,需要注意的是 Successed 和 Error 狀態,需要開發者根據實際情況決定呼叫。

public void loadSuccessed() {
    mCurrentState = State.SUCCESSED;
    cancelAnimation();
    invaidateSelft();
}

public void loadFailed() {
    mCurrentState = State.ERROR;
    cancelAnimation();
    invaidateSelft();
}

將 LoadButton 重置為 Initial 狀態用 reset() 方法。

public void reset(){
    mCurrentState = State.INITIAL;
    rectWidth = getWidth() - mRadiu * 2;
    isUnfold = true;
    cancelAnimation();
    invaidateSelft();
}

到此,整個 LoadButton 實現邏輯已經完成。接下來我們可以編寫程式碼測試。

測試

我們新增一個 LoadButton 到佈局檔案,然後用 3 個 Button 來測試它成功、失敗、重置的情況。
佈局檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp"
    android:orientation="vertical"
    tools:context="com.frank.statusbuttondemo.MainActivity">


    <com.frank.statusbuttondemo.LoadButton
        android:id="@+id/btn_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:backColor="#009966"
        app:contentPaddingLR="20dp"
        app:contentPaddingTB="20dp"
        app:content_color="@android:color/white"
        app:progressedWidth="4dp"
        android:textSize="36sp"
        android:text="點選載入" />
    <Button
        android:id="@+id/btn_test_successed"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="載入成功"/>
    <Button
        android:id="@+id/btn_test_error"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="載入失敗"/>
    <Button
        android:id="@+id/btn_reset"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="重置按鈕"/>
</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    LoadButton mLoadButton;
    Button mBtnSuccessed,mBtnError,mBtnReset;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLoadButton = (LoadButton) findViewById(R.id.btn_status);
        mBtnSuccessed = (Button) findViewById(R.id.btn_test_successed);
        mBtnError = (Button) findViewById(R.id.btn_test_error);
        mBtnReset = (Button) findViewById(R.id.btn_reset);
        mBtnError.setOnClickListener(this);
        mBtnSuccessed.setOnClickListener(this);
        mBtnReset.setOnClickListener(this);

        mLoadButton.setListenner(new LoadButton.LoadListenner() {
            @Override
            public void onClick(boolean isSuccessed) {
                if ( isSuccessed ) {
                    Toast.makeText(MainActivity.this,"載入成功",Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(MainActivity.this,"載入失敗",Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void needLoading() {
                Toast.makeText(MainActivity.this,"重新下載",Toast.LENGTH_LONG).show();
            }
        });


    }

    @Override
    public void onClick(View v) {
        switch (v.getId())
        {
            case R.id.btn_test_successed:
                mLoadButton.loadSuccessed();
                break;

            case R.id.btn_test_error:
                mLoadButton.loadFailed();
                break;
            case R.id.btn_reset:
                mLoadButton.reset();
                break;

            default:
                break;
        }
    }
}

測試結果:
這裡寫圖片描述

總結

本文的主題並不難,但是如果要實現它也需要細心。關鍵是編碼的時候,要先設計分析,之後就是一氣呵成、水到渠成的事情了。

通過演練這個專案,我覺得自己還是有些收穫。

  • 複習了自定義 View 的基本流程。特別是對 onMeasure() 這一塊有更深的理解。
  • 複習了屬性動畫的使用。
  • 複習了 Canvas 和 Path 的基本用法。
  • 演練了狀態模式下的程式設計。
  • 享受回撥機制帶來的美妙感受。

如果有人認為好用,我想把它上傳到 jcenter 倉庫,目的也是為了演練怎麼上傳 Android 模組到開源庫。喜歡這篇文章就給我一個贊吧,需要你們的鼓勵。哈哈。