Android自定義View之可隨時暫停、開啓的圓形下載進度條

請尊重個人勞動成果,轉載註明出處,謝謝!
http://blog.csdn.net/xiaxiazaizai01/article/details/52355558

這是一個一言不合就手擼一個自定義View的任性時代,因此最近一段時間一直在學習自定義View相關的知識,也看了很多與此相關的博客,有句話叫做不要重複造輪子,別人寫好的直接拿過來改吧改吧,能用就行,但是,要想像那些任性的大牛一樣,分分鐘擼一個自定義View,就得不斷的重複造輪子,學習大神們的設計思路, 站在牛人的肩膀上不斷前行,每篇開篇之前都要囉嗦半天,急性子的童鞋可以直接跳過。看到yissan大牛寫了一篇自定義圓形進度條,思路很清晰,就照着也擼了一遍,果然是酸爽啊,在這裏非常感謝yissan大牛,哈哈。。。爲了讓大家能一遍就看懂,我會把註釋寫的非常非常詳細,秒懂哦,,什麼??你不能秒懂。。註釋都寫的辣麼詳細了,面壁思過去。。哈哈

下面看下效果圖:
這裏寫圖片描述

1、首先創建View

(1)設置自定義View屬性,通常做法是在res/values裏面創建一個attrs文件夾,來寫我們的自定義屬性,一般我們設置屬性的name時,一般習慣性的將我們自定義的類名作爲name

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 自定義圓形進度條,屬性設置 -->
    <declare-styleable name="CustomCircleProgress">
        <!-- 默認圓的顏色 -->
        <attr name="progress_default_color" format="color"/>
        <!-- 進度圓的顏色 -->
        <attr name="progress_reached_color" format="color"/>
        <!-- 進度條的高度 -->
        <attr name="progress_reached_height" format="dimension"/>
        <!-- 無進度時(默認圓)的邊框高 -->
        <attr name="progress_default_height" format="dimension"/>
        <!-- 圓的半徑 -->
        <attr name="circle_radius" format="dimension"/>
    </declare-styleable>
</resources>

(2)設置完了自定義屬性,下一步當然是在我們的自定義View類中去獲取。(我們都習慣在參數多的構造方法中去獲取自定義屬性,其他構造方法則去通過this去調用,注意這裏是this而不是super,super的話則指向的是父類,這裏我犯了一個常識性錯誤,一鍵生成幾個構造方法,忘了將super改成this,導致獲取屬性的方法沒有被調用執行,大家在調用的時候可以打斷點試試)

public CustomCircleProgress(Context context) {
        this(context,null);
    }

    public CustomCircleProgress(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomCircleProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取自定義屬性的值
        TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CustomCircleProgress);
        //默認圓的顏色
        mDefaultColor = array.getColor(R.styleable.CustomCircleProgress_progress_default_color, PROGRESS_DEFAULT_COLOR);
        //進度條的顏色
        mReachedColor = array.getColor(R.styleable.CustomCircleProgress_progress_reached_color, PROGRESS_REACHED_COLOR);
        //默認圓的高度
        mDefaultHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_default_height, mDefaultHeight);
        //進度條的高度
        mReachedHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_reached_height, mReachedHeight);
        //圓的半徑
        mRadius = (int) array.getDimension(R.styleable.CustomCircleProgress_circle_radius, mRadius);
        //最後不要忘了回收 TypedArray
        array.recycle();

        //設置畫筆(new畫筆的操作一般不要放在onDraw方法中,因爲在繪製的過程中onDraw方法會被多次調用)
        setPaint();

我們在new我們的畫筆時一般不要在onDraw()方法中去new,因爲view在不斷的繪製過程中onDraw()方法會不斷的被調用,這樣就會造成不停的new我們的畫筆實例。

//設置畫筆
private void setPaint() {
        mPaint = new Paint();
        //下面是設置畫筆的一些屬性
        mPaint.setAntiAlias(true);//抗鋸齒
        mPaint.setDither(true);//防抖動,繪製出來的圖要更加柔和清晰
        mPaint.setStyle(Paint.Style.STROKE);//設置填充樣式
        /**
         *  Paint.Style.FILL    :填充內部
         *  Paint.Style.FILL_AND_STROKE  :填充內部和描邊
         *  Paint.Style.STROKE  :僅描邊
         */
        mPaint.setStrokeCap(Paint.Cap.ROUND);//設置畫筆筆刷類型


    }

2、處理View的佈局,即測量onMeasure( )

當我們在xml文件中給這個view設置android:layout_width=”“android:layout_height=”“屬性爲固定值、wrap_parent、match_parent 時,表明開發者向ViewGroup溝通表明我需要的空間。ViewGroup收到了開發者對View大小的說明,然後ViewGroup會綜合考慮自己的空間大小以及開發者的請求,然後生成兩個MeasureSpec對象(width與height)傳給View,這兩個對象是ViewGroup向子View提出的要求,就相當於告訴子View:“我已經與你的使用者(開發者)商量過了,現在把我們商量確定的結果告訴你,你的寬度不能違反width MeasureSpec對象的要求,你的高度不能違反height MeasureSpec對象的要求,現在,你趕緊根據這個要求確定下自己要多大空間,只許少,不許多哦。”對於超過ViewGroup爲我們分配的空間時,就需要進行測量處理,然後再將處理後的結果反饋給ViewGroup,如果不是很瞭解的話可以點擊查看上一篇博客,有詳細的說明

/**
     * 使用onMeasure方法是因爲我們的自定義圓形View的一些屬性(如:進度條寬度等)都交給用戶自己去自定義了,所以我們需要去測量下
     * 看是否符合要求
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int paintHeight = Math.max(mReachedHeight, mDefaultHeight);//比較兩數,取最大值

        if(heightMode != MeasureSpec.EXACTLY){
            //如果用戶沒有精確指出寬高時,我們就要測量整個View所需要分配的高度了,測量自定義圓形View設置的上下內邊距+圓形view的直徑+圓形描邊邊框的高度
            int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius*2 + paintHeight;
            //然後再將測量後的值作爲精確值傳給父類,告訴他我需要這麼大的空間,你給我分配吧
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
        }
        if(widthMode != MeasureSpec.EXACTLY){
            //這裏在自定義屬性中沒有設置圓形邊框的寬度,所以這裏直接用高度代替
            int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius*2 + paintHeight;
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

我們需要考慮開發者有時會給View設置一些padding屬性

3、繪製View,即onDraw()

(1)這裏我們需要繪製默認的內部圓以及表示進度的外層圓弧,根據進度值的變化來繪製圓弧。在繪製外層表示進度的圓弧時,需要首先確定圓弧的外接矩形(進度也就成了內切圓)的座標,如下圖所示
這裏寫圖片描述

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

        /**
         * 這裏canvas.save();和canvas.restore();是兩個相互匹配出現的,作用是用來保存畫布的狀態和取出保存的狀態的
         * 當我們對畫布進行旋轉,縮放,平移等操作的時候其實我們是想對特定的元素進行操作,但是當你用canvas的方法來進行這些操作的時候,其實是對整個畫布進行了操作,
         * 那麼之後在畫布上的元素都會受到影響,所以我們在操作之前調用canvas.save()來保存畫布當前的狀態,當操作之後取出之前保存過的狀態,
         * (比如:前面元素設置了平移或旋轉的操作後,下一個元素在進行繪製之前執行了canvas.save();和canvas.restore()操作)這樣後面的元素就不會受到(平移或旋轉的)影響
         */
        canvas.save();
        //爲了保證最外層的圓弧全部顯示,我們通常會設置自定義view的padding屬性,這樣就有了內邊距,所以畫筆應該平移到內邊距的位置,這樣畫筆纔會剛好在最外層的圓弧上
        //畫筆平移到指定paddingLeft, getPaddingTop()位置
        canvas.translate(getPaddingLeft(),getPaddingTop());
        mPaint.setStyle(Paint.Style.STROKE);
        //畫默認圓(邊框)的一些設置
        mPaint.setColor(mDefaultColor);
        mPaint.setStrokeWidth(mDefaultHeight);
        canvas.drawCircle(mRadius,mRadius,mRadius,mPaint);

        //畫進度條的一些設置
        mPaint.setColor(mReachedColor);
        mPaint.setStrokeWidth(mReachedHeight);
        //根據進度繪製圓弧
        float sweepAngle = getProgress() * 1.0f / getMax() * 360;
        canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius *2), 0, sweepAngle, false, mPaint);//drawArc:繪製圓弧

        canvas.restore();

    }

我們做個定時器,讓進度條動起來

public class MainActivity extends AppCompatActivity {

    private CustomCircleProgress circleProgress;
    private int progress;

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

        circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);

        Timer timer = new Timer();
         TimerTask task = new TimerTask() {
            @Override
            public void run() {
                if(progress >= 100){
                    progress = 0;
                    circleProgress.setProgress(0);
                }else{
                    progress = circleProgress.getProgress();
                    circleProgress.setProgress(++progress);
                }
            }
        };
        timer.schedule(task,0,100);

    }
}

這樣得到的效果圖是這樣的
這裏寫圖片描述
有的夥計該說了,爲什麼進度條的起始位置不是從最上面開始的,因爲這裏我設置的canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius 2), 0, sweepAngle, false, mPaint);中我設置的參數爲0,表示圓弧的起始位置從0開始,即X軸的正方向。這裏我們只需將圓弧的起始位置設置成-90度即可,canvas.drawArc(new RectF(0, 0, mRadius 2, mRadius *2), -90, sweepAngle, false, mPaint);我們再來看下效果圖
這裏寫圖片描述
完美,,哈哈,,,,

//繪製圓
public void drawCircle (float cx, float cy, float radius, Paint paint)
//參數說明
/**
* cx:圓心的x座標。
  cy:圓心的y座標。
  radius:圓的半徑。
  paint:繪製時所使用的畫筆。
*/

(2)接下來,我們開始繪製裏面的暫停(完成)狀態時的三角形,以及開啓狀態時的兩條豎線,首先我們通過枚舉的方式定義這兩種狀態,並提供set/get方法供外界調用。首先我們需要Path mPath = new Path();然後通過mPath.moveTo()確定三角形的第一個點的座標,然後通過mPath.lineTo()鏈接其他幾個點的座標,如果當我們設置畫筆的樣式爲mPaint.setStyle(Paint.Style.STROKE);則我們需要執行close形成封閉的三角形,或者你也可以直接再來一條mPath.lineTo()再將第一個點的座標給連接起來,這樣也形成了一個封閉的三角形。

//通過path路徑繪製三角形
        mPath = new Path();
        //讓三角形的長度等於圓的半徑(等邊三角形)
        triangleLength = mRadius;
        //繪製三角形,首先我們需要確定三個點的座標
        float firstX = (float) ((mRadius*2 - Math.sqrt(3.0) / 2 * mRadius) / 2);//左上角第一個點的橫座標,根據勾三股四弦五定律,Math.sqrt(3.0)表示開方
        //爲了顯示的好看些,這裏微調下firstX橫座標
        float mFirstX = (float)(firstX + firstX*0.2);
        float firstY = mRadius - triangleLength / 2;
        //同理,依次可得出第二個點(左下角)第三個點的座標
        float secondX = mFirstX;
        float secondY = (float) (mRadius + triangleLength / 2);
        float thirdX = (float) (mFirstX + Math.sqrt(3.0) / 2 * mRadius);
        float thirdY =  mRadius;
        mPath.moveTo(mFirstX,firstY);
        mPath.lineTo(secondX,secondY);
        mPath.lineTo(thirdX,thirdY);
        mPath.lineTo(mFirstX,firstY);

然後我們在onDraw()方法中去判斷繪製不同狀態下的view

//有了path之後就可以在onDraw中繪製三角形的End和Starting狀態了
        if(mStatus == Status.End){//未開始狀態,畫筆填充三角形
            mPaint.setStyle(Paint.Style.FILL);
            //設置顏色
            mPaint.setColor(Color.parseColor("#01A1EB"));
            //畫三角形
            canvas.drawPath(mPath,mPaint);
        }else{//正在進行狀態,畫兩條豎線
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(dp2px(5));
            mPaint.setColor(Color.parseColor("#01A1EB"));
            canvas.drawLine(mRadius*2/3, mRadius*2/3, mRadius*2/3, 2*mRadius*2/3, mPaint);
            canvas.drawLine(2*mRadius - (mRadius*2/3), mRadius*2/3, 2*mRadius - (mRadius*2/3), 2*mRadius*2/3, mPaint);
        }

4、處理與用戶的交互

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.customview.MainActivity">

    <com.example.customview.CustomCircleProgress
        android:id="@+id/circleProgress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="10dp"
        />
</RelativeLayout>

最後是我們的MainActivity類

public class MainActivity extends AppCompatActivity {

    private CustomCircleProgress circleProgress;
    private int progress;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case PROGRESS_CIRCLE_STARTING:
                    progress = circleProgress.getProgress();
                    circleProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(PROGRESS_CIRCLE_STARTING);
                        progress = 0;
                        circleProgress.setProgress(0);
                        circleProgress.setStatus(CustomCircleProgress.Status.End);//修改顯示狀態爲完成
                    }else{
                        //延遲100ms後繼續發消息,實現循環,直到progress=100
                        handler.sendEmptyMessageDelayed(PROGRESS_CIRCLE_STARTING, 100);
                    }
                    break;
            }
        }
    };
    public static final int PROGRESS_CIRCLE_STARTING = 0x110;

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

        circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);

        circleProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(circleProgress.getStatus() == CustomCircleProgress.Status.Starting){//如果是開始狀態
                    //點擊則變成關閉暫停狀態
                    circleProgress.setStatus(CustomCircleProgress.Status.End);
                    //注意,當我們暫停時,同時還要移除消息,不然的話進度不會被停止
                    handler.removeMessages(PROGRESS_CIRCLE_STARTING);
                }else{
                    //點擊則變成開啓狀態
                    circleProgress.setStatus(CustomCircleProgress.Status.Starting);
                    Message message = Message.obtain();
                    message.what = PROGRESS_CIRCLE_STARTING;
                    handler.sendMessage(message);
                }
            }
        });
    }
}

最後,希望對你能有所幫助,有問題歡迎留言,大家一塊探討,寫博客確實挺累的。。。有需要源碼的,可以點擊下載源碼

發佈了29 篇原創文章 · 獲贊 60 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章