用DrawText实现高效的Android倒计时功能。

2024-05-13 06:48

本文主要是介绍用DrawText实现高效的Android倒计时功能。,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一篇博客也说了要实现一个倒计时的自定义控件,这次就把写好的自定义控件给发出来。暂时用着还没有什么问题,功能还较弱,日后可能会继续强化,目前就这样了,觉得还不错的话可以自己修改。

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.CountDownTimeViewandroid: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.CountDownTimeViewandroid: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 {@Overrideprotected 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() {    // 倒计时结束的回调@Overridepublic 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;  // 分隔符的baselineprivate int mHours;         // 小时private int mMinute;        // 分钟private int mSecond;        // 秒private int mViewWidth;     // 控件的宽度private CountDownRunnable mRunnable;    // 倒计时的Runnableprivate 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);}@Overrideprotected 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);}@Overrideprotected 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 {@Overridepublic 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倒计时功能。的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/984971

相关文章

input的accept属性让文件上传安全高效

《input的accept属性让文件上传安全高效》文章介绍了HTML的input文件上传`accept`属性在文件上传校验中的重要性和优势,通过使用`accept`属性,可以减少前端JavaScrip... 目录前言那个悄悄毁掉你上传体验的“常见写法”改变一切的 html 小特性:accept真正的魔法:让

MyBatis-Plus逻辑删除实现过程

《MyBatis-Plus逻辑删除实现过程》本文介绍了MyBatis-Plus如何实现逻辑删除功能,包括自动填充字段、配置与实现步骤、常见应用场景,并展示了如何使用remove方法进行逻辑删除,逻辑删... 目录1. 逻辑删除的必要性编程1.1 逻辑删除的定义1.2 逻辑删php除的优点1.3 适用场景2.

C#借助Spire.XLS for .NET实现在Excel中添加文档属性

《C#借助Spire.XLSfor.NET实现在Excel中添加文档属性》在日常的数据处理和项目管理中,Excel文档扮演着举足轻重的角色,本文将深入探讨如何在C#中借助强大的第三方库Spire.... 目录为什么需要程序化添加Excel文档属性使用Spire.XLS for .NET库实现文档属性管理Sp

Python+FFmpeg实现视频自动化处理的完整指南

《Python+FFmpeg实现视频自动化处理的完整指南》本文总结了一套在Python中使用subprocess.run调用FFmpeg进行视频自动化处理的解决方案,涵盖了跨平台硬件加速、中间素材处理... 目录一、 跨平台硬件加速:统一接口设计1. 核心映射逻辑2. python 实现代码二、 中间素材处

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Python实现快速扫描目标主机的开放端口和服务

《Python实现快速扫描目标主机的开放端口和服务》这篇文章主要为大家详细介绍了如何使用Python编写一个功能强大的端口扫描器脚本,实现快速扫描目标主机的开放端口和服务,感兴趣的小伙伴可以了解下... 目录功能介绍场景应用1. 网络安全审计2. 系统管理维护3. 网络故障排查4. 合规性检查报错处理1.

Python轻松实现Word到Markdown的转换

《Python轻松实现Word到Markdown的转换》在文档管理、内容发布等场景中,将Word转换为Markdown格式是常见需求,本文将介绍如何使用FreeSpire.DocforPython实现... 目录一、工具简介二、核心转换实现1. 基础单文件转换2. 批量转换Word文件三、工具特性分析优点局

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

Java使用Spire.Doc for Java实现Word自动化插入图片

《Java使用Spire.DocforJava实现Word自动化插入图片》在日常工作中,Word文档是不可或缺的工具,而图片作为信息传达的重要载体,其在文档中的插入与布局显得尤为关键,下面我们就来... 目录1. Spire.Doc for Java库介绍与安装2. 使用特定的环绕方式插入图片3. 在指定位

Java使用Spire.Barcode for Java实现条形码生成与识别

《Java使用Spire.BarcodeforJava实现条形码生成与识别》在现代商业和技术领域,条形码无处不在,本教程将引导您深入了解如何在您的Java项目中利用Spire.Barcodefor... 目录1. Spire.Barcode for Java 简介与环境配置2. 使用 Spire.Barco