用DrawText实现高效的Android倒计时功能。
来源:互联网 发布:扣白底图软件 编辑:程序博客网 时间:2024/05/23 01:58
上一篇博客也说了要实现一个倒计时的自定义控件,这次就把写好的自定义控件给发出来。暂时用着还没有什么问题,功能还较弱,日后可能会继续强化,目前就这样了,觉得还不错的话可以自己修改。
2016.01.19修复bug:当设置空间的宽度为WrapContent时,如果小时的位数超过3位,那么会导致控件显示不全,这是因为测量的时候是按两位小时来算的,只要在设置时间的时候请求重新测量就可以了。代码已改,下载Demo的请自行对照博客修改(在setTime那个方法)。
2016.01.28修复bug:不能自己new Handler, View本身自带Handler,可以直接使用。new Handler可能会导致回调重复执行,并且内存溢出
一、成品预览
小时位数的切换和倒计时结束的回调。
可以设置字体大小,颜色,分隔符,分隔符两边的margin。
二、使用方式
1. 首先定义一些自定义属性。
attrs.xml
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="CountDownTimeView"> <attr name="textSize" format="dimension"/> <attr name="textColor" format="color|reference"/> <attr name="division" format="string"/> <attr name="divisionMargin" format="dimension"/> </declare-styleable></resources>
2. 布局文件
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" xmlns:app="http://schemas.android.com/apk/res-auto"> <com.aitsuki.countdowntime.CountDownTimeView android:id="@+id/time_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" app:textSize="60dp" app:textColor="#0f0" app:divisionMargin="3dp" app:division=":"/> <com.aitsuki.countdowntime.CountDownTimeView android:id="@+id/time_view2" android:layout_width="match_parent" android:layout_height="wrap_content" app:textSize="60dp" app:textColor="#f00" app:divisionMargin="3dp" app:division="|"/></LinearLayout>
3. activity中设置时间和回调监听
package com.aitsuki.countdowntime;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.widget.Toast;public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CountDownTimeView time_view = (CountDownTimeView) findViewById(R.id.time_view); time_view.setTime(3000L); // 设置倒计时的时间,long类型,注意超过int最大值后会变成负数,所以最好加上L。 time_view.setOnTimeFinishCallBack(new CountDownTimeView.OnTimeFinishCallBack() { // 倒计时结束的回调 @Override public void timeFinish() { Toast.makeText(MainActivity.this,"倒计时结束了", Toast.LENGTH_SHORT).show(); } }); time_view.start(); // 开始计时 CountDownTimeView time_view2 = (CountDownTimeView) findViewById(R.id.time_view2); long time = 1000*60*60*1000L + 3000; // 设置倒计时的时间,long类型,注意超过int最大值后会变成负数,所以最好加上L。 time_view2.setTime(time); time_view2.start(); }}
三、代码实现
对于DrawText有疑问的,虽然类的文档注释有一些说明,但最好还是去看看我的另外一篇帖子。Android paint的drawText() 的正确使用方式
1. 关于测量
高度一律使用paint.getTextBounds()获得
宽度一律使用paint.measureText()获得
控件的总宽度 = padding + 数字的宽度 + 分隔符的宽度 + 分隔符的margin
控件的总高度 = padding + 数字的高度
数字的baseLine = 控件的总高度 - 数字的bottom - paddingBottom
分隔符baseLine = 控件的总高度 - 分隔符的bottom - paddingBottom + 数字的高度/2 - 分隔符高度 /2
2. 关于Draw
计算好偏移量就可以了
因为是从“控件总宽度/2 - 控件的实际宽度/2”的位置开始画,所以控件默认是水平居中的。
3. 代码
package com.aitsuki.countdowntime;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Rect;import android.os.Handler;import android.text.TextUtils;import android.util.AttributeSet;import android.view.Gravity;import android.view.View;/** * Created by AItsuki on 2015/12/25. * <p/> * 关于 baseLine: * 画文字的位置和画图形的位置是不一样的 * 画图形是从图形的left和top的位置开始往右下方向画,这个不再详细说明 * 而画文字是从文字的左边和文字的baseline往右上方画,所以如果将文字画在0,0 的位置上, * 那么你就只能看到文字底部的一点点了,其实就是baseline下面的一点点内容,这时候y=0其实就是baseline了。 * <p/> * 并且画出来的文字左边会和屏幕边框有一定的距离,那是因为文字本身就是有边距的,可以理解为默认字间距。 * drawText("AItsuki的博客~",0, 0, Paint paint); 可以自己去试试。 * <p/> * drawText(String text,int x, int baseline, Paint paint); * 那么怎么才能将画出来的文字贴合屏幕呢。 * 这就需要计算文字的最小包裹区域了,就是没有算上字间距和行间距的区域。 * Paint提供了一个方法, getTextBounds; 传入一个Rect对象可以获得文字的左上右下(相对于左上角0,0位置)和最小宽高。 * <p/> * 顶部贴合屏幕: * 上面也说过,y=0的位置其实就是baseline,而露出的一点点其实就是rect.bottom。很容易可以得出 * 当baseline = rect.height - rect.bottom的时候,就可以恰好将文字显示完全。 * <p/> * 左边贴合屏幕(不推荐去掉边距,下面有说): * 第一种:减去左边的边距。drawText("AItsuki的博客~",-rect.left , baseline, Paint paint) * 第二种:也是减去左边的边距,换种方式减而已,设置paint.textAlign为center从中间开始画 * paint.setTextAlign(Paint.Align.CENTER); * drawText("AItsuki的博客~",rect.width()/2 , baseline, Paint paint) * 这两种方法都有弊端,所以不推荐使用,如果是一次性画一段文字或者每次只画一个字拼起来没问题, * 但是两个字两个字的画就不太好,因为每个字的宽度都不一样,会导致字和字之间的距离不一致。 * <p/> * 关于rect.width()和 paint.measureText() * 前者是获取最小包裹区域的宽度,后者是获取加上左右边距的宽度。 * 推荐使用后者,因为前者0123456789,各个数字的宽高不一致,测量会出问题。 * 非要用rect.width()的话要分开计算宽高,4是最宽的但是也是最矮,0是最高的但是宽度不够,所以干脆用measureText就行了。 */public class CountDownTimeView extends View { // 有关字体的一些自定义 private float mTextSize = dip2px(60); // 字体大小 private int mTextColor = 0xff000000; // 字体颜色 // 分隔符的一些自定义 private String mDivision = ":"; // 分隔符的样式 private float mDivisionMargin = dip2px(3); // 分隔符的margin // 一些默认初始值 private int mHoursLength = 2; // 小时的位数 private long mOverPlusTime; // 剩余的时间(long) private Paint mTextPaint; //--------------------------------------------------- private float mNumWidth; // 数字的宽度 private int mNumHeight; // 数字的高度 private int mNumBottom; // 数字的底部在屏幕上的位置,关于这个的详细解释看注释 private int mNumBaseLine; // 数字baseline = rect.height()文字的最小高度 - rect.bottom文字底边的距离; 不是真正的baseline,详情请看类描述 private float mDivisionWidth; //分隔符的宽度 private int mDivisionHeight; // 分隔符的高度 private int mDivisionBottom; // 分隔符底边的距离 private int mDivisionBaseLine; // 分隔符的baseline private int mHours; // 小时 private int mMinute; // 分钟 private int mSecond; // 秒 private int mViewWidth; // 控件的宽度 private CountDownRunnable mRunnable; // 倒计时的Runnable private OnTimeFinishCallBack mOnTimeFinishCallBack; // 倒计时结束的回调 private int paddingLeft; private int paddingRight; private int paddingTop; private int paddingBottom; public CountDownTimeView(Context context) { this(context, null); } public CountDownTimeView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CountDownTimeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CountDownTimeView); mTextColor = array.getColor(R.styleable.CountDownTimeView_textColor, mTextColor); mTextSize = array.getDimension(R.styleable.CountDownTimeView_textSize, mTextSize); String division = array.getString(R.styleable.CountDownTimeView_division); mDivisionMargin = array.getDimension(R.styleable.CountDownTimeView_divisionMargin, mDivisionMargin); array.recycle(); if (!TextUtils.isEmpty(division)) { mDivision = division; } //-------------------- // 初始化画笔 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(mTextColor); // 获取文字的最小包裹区域 Rect rect = new Rect(); mTextPaint.getTextBounds("0", 0, 1, rect);// mNumWidth = rect.width(); //不能用这个获取宽度,获取的宽度比实际宽度小,文字默认有边距 mNumWidth = mTextPaint.measureText("0"); // 获取文字占用的宽度 mNumHeight = rect.height(); mNumBottom = rect.bottom; // 计算分隔符 mTextPaint.getTextBounds(mDivision, 0, mDivision.length(), rect);// mDivisionWidth = rect.width(); //不能用这个获取宽度,获取的宽度比实际宽度小,文字默认有边距 mDivisionWidth = mTextPaint.measureText(mDivision); // 获取文字占用的宽度 mDivisionHeight = rect.height(); mDivisionBottom = rect.bottom; setTime(mOverPlusTime); // 设置初始时间 mRunnable = new CountDownRunnable(); // 创建runnable对象 } // 计算一个整数的位数 private int numberLength(int num) { int length = 1; while (num > 9) { length++; num = num / 10; } return length; } /** * dp转px * * @param dip * @return */ private int dip2px(int dip) { float density = getResources().getDisplayMetrics().density; return (int) (dip * density + 0.5f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); paddingLeft = getPaddingLeft(); paddingRight = getPaddingRight(); paddingTop = getPaddingTop(); paddingBottom = getPaddingBottom(); // mNumbaseLine = mNumHeight + paddingTop + paddingBottom - (mNumBottom + paddingBottom) 得到下面 mNumBaseLine = mNumHeight - mNumBottom + paddingTop; mDivisionBaseLine = mDivisionHeight - mDivisionBottom + paddingTop + mNumHeight / 2 - mDivisionHeight / 2; mViewWidth = getContentWidth(); mViewWidth = resolveSize(mViewWidth, widthMeasureSpec); int mViewHeight = resolveSize(mNumHeight + paddingTop + paddingBottom, heightMeasureSpec); setMeasuredDimension(mViewWidth, mViewHeight); } @Override protected void onDraw(Canvas canvas) { float hoursWidth = mNumWidth * mHoursLength; float minuteWidth = mNumWidth * 2; // float secondWidth = mNumWidth * 2; 暂时没用 // 如果控件的长变短了(例:100小时 --> 99 小时),设置left,让控件居中。 int contentWidth = getContentWidth(); float hoursLeft = mViewWidth > contentWidth ? mViewWidth / 2 - contentWidth / 2 : 0 + paddingLeft; float minuteLeft = hoursLeft + hoursWidth + mDivisionWidth + mDivisionMargin * 2; float secondLeft = minuteLeft + minuteWidth + mDivisionWidth + mDivisionMargin * 2; // 小时 canvas.drawText(formatNum(mHours), hoursLeft, mNumBaseLine, mTextPaint); canvas.drawText(mDivision, hoursLeft + hoursWidth + mDivisionMargin, mDivisionBaseLine, mTextPaint); // 分钟 canvas.drawText(formatNum(mMinute), minuteLeft, mNumBaseLine, mTextPaint); canvas.drawText(mDivision, minuteLeft + minuteWidth + mDivisionMargin, mDivisionBaseLine, mTextPaint); // 秒 canvas.drawText(formatNum(mSecond), secondLeft, mNumBaseLine, mTextPaint); } private String formatNum(int num) { return num < 10 ? "0" + num : String.valueOf(num); } /** * 计算控件的总长度 * * @return */ private int getContentWidth() { float width = mDivisionWidth * 2 + mDivisionMargin * 4 + mNumWidth * (4 + mHoursLength); width += paddingLeft + paddingRight; return (int) Math.ceil(width); } /** * 设置结束的回调侦听 * * @param callBack */ public void setOnTimeFinishCallBack(OnTimeFinishCallBack callBack) { this.mOnTimeFinishCallBack = callBack; } /** * 开始倒计时 */ public void start() { postDelayed(mRunnable, 1000); } /** * 设置倒计时的总时长 * * @param ms */ public void setTime(long ms) { ms = ms < 0? 0 : ms; // 时间不能为负数,如果设置时间的时候使用int类型,并且超过int的最大值可能会出现负数。 this.mOverPlusTime = ms; mHours = (int) (ms / (60 * 60 * 1000)); mMinute = (int) (ms % (60 * 60 * 1000) / (60 * 1000)); mSecond = (int) (ms % (60 * 60 * 1000) % (60 * 1000) / 1000); int length = numberLength(mHours); length = length == 1 ? 2 : length; // 如果小时长度发生了变化,需要重新测量。 if(length != mHoursLength) { requestLayout(); } mHoursLength = length; } class CountDownRunnable implements Runnable { @Override public void run() { mOverPlusTime -= 1000; if (mOverPlusTime > 0) { setTime(mOverPlusTime); invalidate(); postDelayed(mRunnable, 1000); } else { if (mOverPlusTime == 0) { setTime(mOverPlusTime); invalidate(); } removeCallbacks(mRunnable); if (mOnTimeFinishCallBack != null) { mOnTimeFinishCallBack.timeFinish(); } } } } public interface OnTimeFinishCallBack { void timeFinish(); }}
4. Demo下载地址
http://download.csdn.net/detail/u010386612/9380742
四、后话
感觉这个自定义控件写出来并没有什么用,没有哪款APP会有这么奇葩的倒计时需求吧,很可惜博主我就遇上了……
这个控件写的比较简单,注释也算完整,希望能和各位网友共同学习交流。
本来想写一个系列的View 到viewGroup的基础和进阶,但是看到这类型的博文太多了,我不觉得会写的比他们好, 所以也就放弃了,之后估计会一直写一些自定义控件,希望大家多多支持。
- 用DrawText实现高效的Android倒计时功能。
- Android倒计时功能的实现
- Android 倒计时功能的实现
- android倒计时功能的实现
- android 实现倒计时功能
- Android倒计时功能实现
- android实现倒计时功能
- Android 倒计时功能实现
- Android实现倒计时功能
- android开发中倒计时功能的实现
- android倒计时功能的实现(CountDownTimer)
- android倒计时功能的实现(CountDownTimer)
- android倒计时功能的实现(CountDownTimer)
- android倒计时功能的实现(CountDownTimer)
- android倒计时功能的实现(CountDownTimer)
- android倒计时功能的实现(CountDow…
- android倒计时功能的实现(CountDownTimer)
- Android开发中倒计时功能的实现
- ubuntu给指定用户添加sudo权限
- Python整数的缓存
- 进击的Android之异步加载
- delphi根据汉字生成拼音,全拼,或者带空格,或者不带空格
- 获取所有的provider
- 用DrawText实现高效的Android倒计时功能。
- 微信应用开发记录
- 修改ubuntu默认开机亮度
- PHP内存泄漏检测方法
- 黑马52期学后总结笔记(十一)
- 创建队列 NSOperationQueue dispatch队列组
- Android SQLite数据库 《第一行代码》
- 最令人发指叫人吐血的代码风格
- Cocos-2dx台球游戏实现