請尊重個人勞動成果,轉載註明出處,謝謝!
http://blog.csdn.net/xiaxiazaizai01/article/details/52415377
上一篇寫了一個可隨時暫停的圓形進度條,接下來再來擼一個帶小圓圈的倒計時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屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CountDownProgress">
<!-- 默認圓實心的顏色 -->
<attr name="default_circle_solide_color" format="color"/>
<!-- 默認圓邊框的顏色 -->
<attr name="default_circle_stroke_color" format="color"/>
<!-- 默認圓邊框的寬度 -->
<attr name="default_circle_stroke_width" format="dimension"/>
<!-- 默認圓的半徑 -->
<attr name="default_circle_radius" format="dimension"/>
<!-- 進度條的顏色 -->
<attr name="progress_color" format="color"/>
<!-- 進度條的寬度 -->
<attr name="progress_width" format="dimension"/>
<!-- 小圓的實心顏色 -->
<attr name="small_circle_solide_color" format="color"/>
<!-- 小圓的邊框顏色 -->
<attr name="small_circle_stroke_color" format="color"/>
<!-- 小圓的邊框寬度 -->
<attr name="small_circle_stroke_width" format="dimension"/>
<!-- 小圓的半徑 -->
<attr name="small_circle_radius" format="dimension"/>
<!-- 文字的顏色 -->
<attr name="text_color" format="color"/>
<!-- 文字的字體大小 -->
<attr name="text_size" format="dimension"/>
</declare-styleable>
</resources>
(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++){
int attr = typedArray.getIndex(i);
switch (attr){
case R.styleable.CountDownProgress_default_circle_solide_color:
defaultCircleSolideColor = typedArray.getColor(attr, defaultCircleSolideColor);
break;
case R.styleable.CountDownProgress_default_circle_stroke_color:
defaultCircleStrokeColor = typedArray.getColor(attr, defaultCircleStrokeColor);
break;
case R.styleable.CountDownProgress_default_circle_stroke_width:
defaultCircleStrokeWidth = (int) typedArray.getDimension(attr, defaultCircleStrokeWidth);
break;
case R.styleable.CountDownProgress_default_circle_radius:
defaultCircleRadius = (int) typedArray.getDimension(attr, defaultCircleRadius);
break;
case R.styleable.CountDownProgress_progress_color:
progressColor = typedArray.getColor(attr, progressColor);
break;
case R.styleable.CountDownProgress_progress_width:
progressWidth = (int) typedArray.getDimension(attr, progressWidth);
break;
case R.styleable.CountDownProgress_small_circle_solide_color:
smallCircleSolideColor = typedArray.getColor(attr, smallCircleSolideColor);
break;
case R.styleable.CountDownProgress_small_circle_stroke_color:
smallCircleStrokeColor = typedArray.getColor(attr, smallCircleStrokeColor);
break;
case R.styleable.CountDownProgress_small_circle_stroke_width:
smallCircleStrokeWidth = (int) typedArray.getDimension(attr, smallCircleStrokeWidth);
break;
case R.styleable.CountDownProgress_small_circle_radius:
smallCircleRadius = (int) typedArray.getDimension(attr, smallCircleRadius);
break;
case R.styleable.CountDownProgress_text_color:
textColor = typedArray.getColor(attr, textColor);
break;
case R.styleable.CountDownProgress_text_size:
textSize = (int) typedArray.getDimension(attr, textSize);
break;
}
}
//回收typedArray對象
typedArray.recycle();
//設置畫筆
setPaint();
}
設置畫筆的方法,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)爲座標原點,那麼小圓在幾個特殊點的左邊,在圖中我已經標出來了,爲什麼要標記小圓運動到這幾個特殊點的座標,這是因爲小圓是隨着進度條的運動而運動的,我們要通過這些座標計算分析得出小圓的圓心座標的變化規律。後面會說到。
首先我們還是來一步步實現,首先我們先不考慮小圓,實現一箇中間帶文字進度變化的圓形進度條,如下圖所示
辣麼,接下來是代碼展示了,爲了方便進度計算,我們讓我們的自定義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如何使用
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.customcountdownprogress.CountDownProgress
android:id="@+id/countdownProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="20dp"
/>
</RelativeLayout>
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);*/
}
});
}
}