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

接下來我們就按著自定義View的五步走,實現上圖的效果。什麼??你不知道哪五步,好吧,那我就引用下yissan小伙伴博客中提到的五步走。
辣麼,接下來我們就開始一步步實現這個效果了。
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);
}/**
* 如果該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);
}
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();
}
//屬性動畫
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);*/
}
});
}
}
Android官方開發文檔Training系列課程中文版:如何避免ANR?
盡管你寫代碼可能通過了世界上所有的性能測試,但是它還是可能會讓人感覺到卡頓。當應用卡的不成樣子時,系統會給你彈一個”Application Not Respo
Android Studio 2.2 apk逆向工具--APK Analyzer
APK解析器解析APKAndroid Studio的APK解析器可以直接查看打包完成的apk的組成。APK解析器可以減少調試app內部的DEX文件和資源文件的時間,對減小
關於android源碼的使用心得體會
小生做程序也有些許日子,從一個青澀的小白,慢慢的成長為了小有成就的程序猿,從不知名的碼農,到二三百人圈裡還有點小名氣的碼霸。 要說辛苦,可能每個程序心中都有各自的理解,大
Android仿支付寶支付從底部彈窗效果
我們再用支付寶支付的時候,會從底部彈上來一個對話框,讓我們選擇支付方式等等,今天我們就來慢慢實現這個功能效果圖實現主界面很簡單,就是一個按鈕,點擊後跳到支付詳情的Frag