Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義View之帶小圓圈的倒計時圓形進度條

Android自定義View之帶小圓圈的倒計時圓形進度條

編輯:關於Android編程

上一篇寫了一個可隨時暫停的圓形進度條,接下來再來撸一個帶小圓圈的倒計時View,主要難點是對於隨著進度條變化而變化的小圓的繪制。看了givemeacondom大神寫的小圓的繪制,大神是通過小圓運動在第一象限、第二象限等不同象限內的四種不同情況來繪制的,說實話,,數學忘的差不多了,好多公式著實是看不懂,再加上原作者注釋的又很少,看的花都謝了。。。最後還是放棄了,這裡非常感謝群裡的yissan大神,他給我提供了一個思路,他說根據進度的變化算出小圓的x、y坐標的變化,於是乎,我又拾起了課本,溫習了一下弧度、正弦sinα、余弦cosα,從而巧妙的將小圓繪制粗來了。在這裡向yissan小伙伴表示感謝。也非常感謝givemeacondom大神給出的創意,我在作者的基礎上,通過自己的想法簡化了復雜的坐標計算。喜歡原文的可以點擊givemeacondom,本文中我會把注釋寫的詳細些,大家可以畫畫圖配合著理解,因為。。代碼和圖更配哦,廢話不多說,老規矩,先來一張效果圖。
這裡寫圖片描述

接下來我們就按著自定義View的五步走,實現上圖的效果。什麼??你不知道哪五步,好吧,那我就引用下yissan小伙伴博客中提到的五步走。

根據Android Developers官網的介紹,自定義控件你需要以下的步驟。(根據你的需要,某些步驟可以省略)

1、創建View

2、處理View的布局

3、繪制View

4、與用戶進行交互

5、優化已定義的View

辣麼,接下來我們就開始一步步實現這個效果了。

1、創建View

(1)自定義view屬性,我們在res/values下面新建一個attr.xml文件,設置我們的自定義view屬性



    

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

(2)在我們的自定義View類中去獲取這些屬性

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

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

    public CountDownProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取自定義屬性
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CountDownProgress);
        int indexCount = typedArray.getIndexCount();
        for(int i=0;i<indexcount;i++){ attr="typedArray.getIndex(i);" case="" defaultcircleradius="(int)" defaultcirclesolidecolor="typedArray.getColor(attr," defaultcirclestrokecolor="typedArray.getColor(attr," defaultcirclestrokewidth="(int)" int="" pre="" progresscolor="typedArray.getColor(attr," progresswidth="(int)" r.styleable.countdownprogress_default_circle_radius:="" r.styleable.countdownprogress_default_circle_solide_color:="" r.styleable.countdownprogress_default_circle_stroke_color:="" r.styleable.countdownprogress_default_circle_stroke_width:="" r.styleable.countdownprogress_progress_color:="" r.styleable.countdownprogress_progress_width:="" r.styleable.countdownprogress_small_circle_radius:="" r.styleable.countdownprogress_small_circle_solide_color:="" r.styleable.countdownprogress_small_circle_stroke_color:="" r.styleable.countdownprogress_small_circle_stroke_width:="" r.styleable.countdownprogress_text_color:="" r.styleable.countdownprogress_text_size:="" smallcircleradius="(int)" smallcirclesolidecolor="typedArray.getColor(attr," smallcirclestrokecolor="typedArray.getColor(attr," smallcirclestrokewidth="(int)" switch="" textcolor="typedArray.getColor(attr," textsize="(int)">

設置畫筆的方法,new畫筆的操作不要在onDraw()方法中進行

private void setPaint() {
        //默認圓
        defaultCriclePaint = new Paint();
        defaultCriclePaint.setAntiAlias(true);//抗鋸齒
        defaultCriclePaint.setDither(true);//防抖動
        defaultCriclePaint.setStyle(Paint.Style.STROKE);
        defaultCriclePaint.setStrokeWidth(defaultCircleStrokeWidth);
        defaultCriclePaint.setColor(defaultCircleStrokeColor);//這裡先畫邊框的顏色,後續再添加畫筆畫實心的顏色
        //默認圓上面的進度弧度
        progressPaint = new Paint();
        progressPaint.setAntiAlias(true);
        progressPaint.setDither(true);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(progressWidth);
        progressPaint.setColor(progressColor);
        progressPaint.setStrokeCap(Paint.Cap.ROUND);//設置畫筆筆刷樣式
        //進度上面的小圓
        smallCirclePaint = new Paint();
        smallCirclePaint.setAntiAlias(true);
        smallCirclePaint.setDither(true);
        smallCirclePaint.setStyle(Paint.Style.STROKE);
        smallCirclePaint.setStrokeWidth(smallCircleStrokeWidth);
        smallCirclePaint.setColor(smallCircleStrokeColor);
           //畫進度上面的小圓的實心畫筆(主要是將小圓的實心顏色設置成白色)
        smallCircleSolidePaint = new Paint();
        smallCircleSolidePaint.setAntiAlias(true);
        smallCircleSolidePaint.setDither(true);
        smallCircleSolidePaint.setStyle(Paint.Style.FILL);
        smallCircleSolidePaint.setColor(smallCircleSolideColor);

        //文字畫筆
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setDither(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
    }

2、處理View的布局(也就是測量onMeasure)

/**
     * 如果該View布局的寬高開發者沒有精確的告訴,則需要進行測量,如果給出了精確的寬高則我們就不管了
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize;
        int heightSize;
        int strokeWidth = Math.max(defaultCircleStrokeWidth, progressWidth);
        if(widthMode != MeasureSpec.EXACTLY){
            widthSize = getPaddingLeft() + defaultCircleRadius*2 + strokeWidth + getPaddingRight();
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        }
        if(heightMode != MeasureSpec.EXACTLY){
            heightSize = getPaddingTop() + defaultCircleRadius*2 + strokeWidth + getPaddingBottom();
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

3、繪制View,即onDraw()

這裡為了能讓大家看的更明白,我粗略的畫了個坐標圖,這裡我們以手機左上角為坐標原點,大圓的圓心坐標為(r,r),而對於小圓的運動軌跡,你也可以以大圓的圓心(r,r)為坐標原點進行分析,這裡我仍是以左上角(0,0)為坐標原點,那麼小圓在幾個特殊點的左邊,在圖中我已經標出來了,為什麼要標記小圓運動到這幾個特殊點的坐標,這是因為小圓是隨著進度條的運動而運動的,我們要通過這些坐標計算分析得出小圓的圓心坐標的變化規律。後面會說到。

這裡寫圖片描述vcq1z9ajrMrXz8jO0sPHz8iyu7+8wsfQodSyo6zKtc/W0ru49tbQvOS0+M7E19a9+LbIseS7r7XE1LLQzr34tsjM9aOsyOfPws28y/nKvg0KPGltZyBhbHQ9"這裡寫圖片描述" data-cke-saved-src="/uploadfile/Collfiles/20160903/20160903091930353.gif" src="/uploadfile/Collfiles/20160903/20160903091930353.gif" title="\"> 辣麼,接下來是代碼展示了,為了方便進度計算,我們讓我們的自定義view繼承ProgressBar,而ProgressBar帶有getProgress()、getMax()方法,從而可以計算出最外層的進度條圓弧掃過的角度currentAngle = getProgress()*1.0f/getMax()*360

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

        canvas.save();
        canvas.translate(getPaddingLeft(), getPaddingTop());
        //畫默認圓
        canvas.drawCircle(defaultCircleRadius,defaultCircleRadius,defaultCircleRadius,defaultCriclePaint);

        //畫進度圓弧
        currentAngle = getProgress()*1.0f/getMax()*360;
        canvas.drawArc(new RectF(0,0,defaultCircleRadius*2,defaultCircleRadius*2),mStartSweepValue, currentAngle ,false,progressPaint);
        //畫中間文字
        String text = getProgress()+"%";
        //獲取文字的長度的方法
        float textWidth = textPaint.measureText(text );
        float textHeight = (textPaint.descent() + textPaint.ascent()) / 2;
        canvas.drawText(text, defaultCircleRadius-textWidth/2, defaultCircleRadius-textHeight, textPaint);

        canvas.restore();

    }

接下來是讓進度條圓弧以及中間的文字動起來(這已經屬於第四步,與用戶進行交互)

public class MainActivity extends AppCompatActivity {

    private CountDownProgress countDownProgress;
    private int progress;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case HANDLER_MESSAGE:
                    progress = countDownProgress.getProgress();
                    countDownProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(HANDLER_MESSAGE);
                        progress = 0;
                        countDownProgress.setProgress(0);
                    }else{
                        handler.sendEmptyMessageDelayed(HANDLER_MESSAGE, 100);
                    }
                    break;
            }
        }
    };
    public static final int HANDLER_MESSAGE = 2;

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

        countDownProgress = (CountDownProgress) findViewById(R.id.countdownProgress);
        countDownProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Message message = Message.obtain();
                message.what = HANDLER_MESSAGE;
                handler.sendMessage(message);
            }
        });
    }
}

接下來我們實現帶小圓的繪制,我們知道由正余弦可以得出 X = cosα * r (r:半徑),Y = sinα * r ,以及 弧度 = 度 * π / 180,而π在Android中用Math.PI表示,再根據上面我們畫的坐標圖中小圓運動到圖中幾個特殊點的坐標可以得出小圓的X、Y坐標的規律:X = sinα * r + r,Y = r - cosα * r,按照此規律就不難算出小圓的坐標變化了。

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

        canvas.save();
        canvas.translate(getPaddingLeft(), getPaddingTop());
        //畫默認圓
        canvas.drawCircle(defaultCircleRadius,defaultCircleRadius,defaultCircleRadius,defaultCriclePaint);

        //畫進度圓弧
        //currentAngle = getProgress()*1.0f/getMax()*360;
        canvas.drawArc(new RectF(0,0,defaultCircleRadius*2,defaultCircleRadius*2),mStartSweepValue, 360*currentAngle,false,progressPaint);
        //畫中間文字
     //   String text = getProgress()+"%";
        //獲取文字的長度的方法
        float textWidth = textPaint.measureText(textDesc);
        float textHeight = (textPaint.descent() + textPaint.ascent()) / 2;
        canvas.drawText(textDesc, defaultCircleRadius-textWidth/2, defaultCircleRadius-textHeight, textPaint);

        //畫小圓
        float currentDegreeFlag = 360*currentAngle + extraDistance;
        float smallCircleX = 0,smallCircleY = 0;
        float hudu = (float) Math.abs(Math.PI * currentDegreeFlag / 180);//Math.abs:絕對值 ,Math.PI:表示π , 弧度 = 度*π / 180
        smallCircleX = (float) Math.abs(Math.sin(hudu) * defaultCircleRadius + defaultCircleRadius);
        smallCircleY = (float) Math.abs(defaultCircleRadius -Math.cos(hudu) * defaultCircleRadius);
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleRadius, smallCirclePaint);
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleRadius - smallCircleStrokeWidth, smallCircleSolidePaint);//畫小圓的實心

        canvas.restore();

    }

上面說了,如果我們的自定義view繼承的不是ProgressBar,則ProgressBar的一些方法我們就用不了了,這裡我們直接繼承View,辣麼,進度條圓弧掃過的角度我們可以用屬性動畫來實現。注釋在代碼中相當詳細,這回你可以秒懂了吧。。

//屬性動畫
    public void startCountDownTime(final OnCountdownFinishListener countdownFinishListener){
        setClickable(false);
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1.0f);
        //動畫時長,讓進度條在CountDown時間內正好從0-360走完,這裡由於用的是CountDownTimer定時器,倒計時要想減到0則總時長需要多加1000毫秒,所以這裡時間也跟著+1000ms
        animator.setDuration(countdownTime+1000);
        animator.setInterpolator(new LinearInterpolator());//勻速
        animator.setRepeatCount(0);//表示不循環,-1表示無限循環
        //值從0-1.0F 的動畫,動畫時長為countdownTime,ValueAnimator沒有跟任何的控件相關聯,那也正好說明ValueAnimator只是對值做動畫運算,而不是針對控件的,我們需要監聽ValueAnimator的動畫過程來自己對控件做操作
        //添加監聽器,監聽動畫過程中值的實時變化(animation.getAnimatedValue()得到的值就是0-1.0)
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                /**
                 * 這裡我們已經知道ValueAnimator只是對值做動畫運算,而不是針對控件的,因為我們設置的區間值為0-1.0f
                 * 所以animation.getAnimatedValue()得到的值也是在[0.0-1.0]區間,而我們在畫進度條弧度時,設置的當前角度為360*currentAngle,
                 * 因此,當我們的區間值變為1.0的時候弧度剛好轉了360度
                 */
                currentAngle = (float) animation.getAnimatedValue();
         //       Log.e("currentAngle",currentAngle+"");
                invalidate();//實時刷新view,這樣我們的進度條弧度就動起來了
            }
        });
        //開啟動畫
        animator.start();
        //還需要另一個監聽,監聽動畫狀態的監聽器
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                //倒計時結束的時候,需要通過自定義接口通知UI去處理其他業務邏輯
                if(countdownFinishListener != null){
                    countdownFinishListener.countdownFinished();
                }
                if(countdownTime > 0){
                    setClickable(true);
                }else{
                    setClickable(false);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
        //調用倒計時操作
        countdownMethod();
    }

實現倒計時,我們這裡用Android系統提供的CountDownTimer實現,下面簡單介紹下CountDownTimer的使用,第一個參數是總時間,第二個是每隔多長時間執行一次onTick方法,注意,這兩個參數值都是以毫秒為單位。在測試的時候發現用CountDownTimer時,倒計時不能到0的情況,下面貼出CountDownTimer的部分源碼,查看源碼發現,當mMillisInFuture = 0的時候直接執行了onFinish方法,大家可以調試的時候查看log打印日志

public synchronized final CountDownTimer start() {
        mCancelled = false;
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

下面把倒計時的代碼貼出來

//倒計時的方法
    private void countdownMethod(){
        new CountDownTimer(countdownTime+1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
       //         Log.e("time",countdownTime+"");
                countdownTime = countdownTime-1000;
                textDesc = countdownTime/1000 + "″";
                //countdownTime = countdownTime-1000;
                Log.e("time",countdownTime+"");
                //刷新view
                invalidate();
            }
            @Override
            public void onFinish() {
                //textDesc = 0 + "″";
                textDesc = "時間到";
                //同時隱藏小球
                smallCirclePaint.setColor(getResources().getColor(android.R.color.transparent));
                smallCircleSolidePaint.setColor(getResources().getColor(android.R.color.transparent));
                //刷新view
                invalidate();
            }
        }.start();
    }

對於希望從什麼時間開始倒計時,我們交給開發者自己去決定,所以這裡我們提供個供外界設置倒計時總時間的方法

public void setCountdownTime(long countdownTime){
        this.countdownTime = countdownTime;
        textDesc = countdownTime / 1000 + "″";
    }

當倒計時結束後,我們需要提供個接口去告訴UI,下面該你處理一些邏輯了

public interface OnCountdownFinishListener{
        void countdownFinished();
    }

最後,再看下我們的布局文件以及MainActivity如何使用

MainActivity

public class MainActivity extends AppCompatActivity {

    private CountDownProgress countDownProgress;
    private int progress;

    /*private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case HANDLER_MESSAGE:
                    progress = countDownProgress.getProgress();
                    countDownProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(HANDLER_MESSAGE);
                        progress = 0;
                        countDownProgress.setProgress(0);
                    }else{
                        handler.sendEmptyMessageDelayed(HANDLER_MESSAGE, 100);
                    }
                    break;
            }
        }
    };
    public static final int HANDLER_MESSAGE = 2;*/

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

        countDownProgress = (CountDownProgress) findViewById(R.id.countdownProgress);
        countDownProgress.setCountdownTime(10*1000);
        countDownProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                countDownProgress.startCountDownTime(new CountDownProgress.OnCountdownFinishListener() {
                    @Override
                    public void countdownFinished() {
                        Toast.makeText(MainActivity.this, "倒計時結束了--->該UI處理界面邏輯了", Toast.LENGTH_LONG).show();
                    }
                });
                /*Message message = Message.obtain();
                message.what = HANDLER_MESSAGE;
                handler.sendMessage(message);*/
            }
        });
    }
}
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved