自定义带动画的日期时间控件

    xiaoxiao2025-04-19  7

    前言代码分析帮助类

    前言

    闲来无聊,写了一个带动画的日期时间控件,分享一下,具体效果如下:

    原理 原理挺简单,拿到当前时间和下一秒的最新时间,当前时间显示在课件位置,新的时间绘制在下方不可见的区域;因为涉及到单字符的动画,所以,在时间绘制的时候,我们将“2016年08月14日 16时58分50秒”拆解成一个个的单文本,然后一个个的绘制,如果当前时间中的文本和下一个新的时间的文本那个字符不一样,我们就将对应的字符走动画,动态的切换,如下图: 为了看的更清楚,将整个控件的高度增加了一倍,把我们看不见的地方的文本也显示出来,方便观察原理。 如上图总所看到,时间从“17时09分59秒”变换到“17时10分00秒”时,我们需要将09变成10,将59变成00,这4个字符是不一样的,就需要动态的去改变他们绘制的Y轴的坐标,从而达到慢慢滑上去的感觉,因此,我们使用ValueAnimator,渐变的将移动的步长从0变换到整个控件的高度,变换值的过程中不断的去重绘,从而达到动的效果。

    代码分析

    自定义属性 在value文件夹下定义该view的自定义属性time_view_attr.xml:

    <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="TimeView"> <!--字体大小--> <attr name="textSize" format="dimension"></attr> <!--字体颜色--> <attr name="textColor" format="color"></attr> <!--时间显示样式--> <attr name="format" format="enum"> <enum name="FORMAT_SHORT" value="0"></enum> <enum name="FORMAT_LONG" value="1"></enum> <enum name="FORMAT_FULL_NOLINE" value="2"></enum> <enum name="FORMAT_SHORT_NOLINE" value="3"></enum> <enum name="FORMAT_SHORT_CN" value="4"></enum> <enum name="FORMAT_LONG_CN" value="5"></enum> </attr> </declare-styleable> </resources>

    其中枚举样式具体的值分别如下:

    FORMAT_VALUES.put(0, "yyyy-MM-dd");//FORMAT_SHORT FORMAT_VALUES.put(1, "yyyy-MM-dd HH:mm:ss");//FORMAT_LONG FORMAT_VALUES.put(2, "yyyyMMddHHmmss");//FORMAT_FULL_NOLINE FORMAT_VALUES.put(3, "yyyyMMdd");//FORMAT_SHORT_NOLINE FORMAT_VALUES.put(4, "yyyy年MM月dd");//FORMAT_SHORT_CN FORMAT_VALUES.put(5, "yyyy年MM月dd日 HH时mm分ss秒");//FORMAT_LONG_CN

    自定义view

    获取自定义参数的值

    private void getStyleAttr(AttributeSet attrs) { TypedArray array = null; //获取自定义参数的值 try { array = getContext().obtainStyledAttributes(attrs, R.styleable.TimeView); //字体大小 textSize = array.getDimensionPixelSize(R.styleable.TimeView_textSize, 20); //字体颜色 textColor = array.getColor(R.styleable.TimeView_textColor, Color.parseColor("#eeeeee")); //日期显示的样式 dateFormat = FORMAT_VALUES.get(array.getInt(R.styleable.TimeView_format, 6)); } catch (Exception e) { //一旦异常,给定一个默认值 textSize = 20; textColor = Color.parseColor("#eeeeee"); dateFormat = FORMAT_VALUES.get(6); } }

    起线程1S刷新一次时间 每一秒钟去更新一下时间,然后重新开启一个ValueAnimator动画去渐变改变步长。

    //没1秒获取时间的线程 private Runnable timer = new Runnable() { @Override public void run() { newTimeTxt = DateUtil.format(new Date(), dateFormat); changeTimeAmin(); handler.postDelayed(this, 1000); } }; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //一开始就开启一个计时器 1秒之后更新时间 handler.postDelayed(timer, 1000); }

    添加valueAnimator动画

    /** * 更新滚动步长的动画 */ public void changeTimeAmin() { ValueAnimator animator = ValueAnimator.ofInt(0, height); animator.setDuration(800); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { rollStep = (int) animation.getAnimatedValue(); if (rollStep >= height) { presentTxt = newTimeTxt; newTimeTxt = DateUtil.format(new Date(), dateFormat); } else invalidate(); } }); animator.start(); }

    绘制

    @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); presentTime.clear(); newTime.clear(); //将时间的文本拆解成一个单文本的集合,以便于动态的更新单个字符的动画 for (int i = 0; i < presentTxt.length(); i++) { presentTime.add(presentTxt.substring(i, i + 1)); if (null != newTimeTxt && newTimeTxt.length() > 0) newTime.add(newTimeTxt.substring(i, i + 1)); } int leftX = 0; //循环绘制一个个的单字符 for (int i = 0; i < presentTime.size(); i++) { //绘制最左边的时候,将paddingLeft加上去 if (i == 0) leftX += getPaddingLeft(); //如果是第一次绘制的时候,就只有当前时间 没有新的时间 if (null != newTime && newTime.size() > 0) { //如果最新的时间不为空的时候 //单个字符相同,说明单字符对应地方的时间没有变化 //比如20160813164400-->20160813164401,就只需要最后的1需要从0-->1,其他的都不需要动 if (presentTime.get(i).equals(newTime.get(i))) { //这里就是绘制2016081316440这部分不需要有动画的部分 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop(), mPaint); } else { //这里就是绘制最后的那个0-->1的动画 //因为changeTimeAmin()方法中的动画一直在不停的改变rollStep的值并不停的在重绘,一次就产生的动的效果 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop() - rollStep, mPaint); canvas.drawText(newTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop() + height - rollStep, mPaint); } } else //只绘制当前时间的文本 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop(), mPaint); leftX += mPaint.measureText(presentTime.get(i)); } }

    view的全代码

    import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Handler; import android.util.AttributeSet; import android.view.View; import com.lpf.afeidemo.R; import com.lpf.afeidemo.utils.DateUtil; import java.util.ArrayList; import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Map; /** * Created by asus on 2016/8/14. */ public class TimeView extends View { private static final String TAG = TimeView.class.getSimpleName(); //当前时间的文本 private String presentTxt; //下一个新的时间文本 private String newTimeTxt; //字体的大小 private int textSize; //字体的颜色 private int textColor; //控件的宽 private int width; //控件的高 private int height; //当前滚动的步长 private int rollStep = 0; //画笔对象 private Paint mPaint; //文本测量对象 private Paint.FontMetrics fontMetrics; //当前的时间文本拆分后的单文本集合 private List<String> presentTime = new ArrayList<>(); //下一个新的时间拆分后的单文本集合 private List<String> newTime = new ArrayList<>(); //日期的展示形式 private String dateFormat = DateUtil.FORMAT_FULL_NOLINE; private Handler handler = new Handler(); //枚举对应的文本展示形式的值 private static final Map<Integer, String> FORMAT_VALUES = new Hashtable<>(); //静态赋值 static { FORMAT_VALUES.put(0, "yyyy-MM-dd");//FORMAT_SHORT FORMAT_VALUES.put(1, "yyyy-MM-dd HH:mm:ss");//FORMAT_LONG FORMAT_VALUES.put(2, "yyyyMMddHHmmss");//FORMAT_FULL_NOLINE FORMAT_VALUES.put(3, "yyyyMMdd");//FORMAT_SHORT_NOLINE FORMAT_VALUES.put(4, "yyyy年MM月dd");//FORMAT_SHORT_CN FORMAT_VALUES.put(5, "yyyy年MM月dd日 HH时mm分ss秒");//FORMAT_LONG_CN } //没1秒获取时间的线程 private Runnable timer = new Runnable() { @Override public void run() { newTimeTxt = DateUtil.format(new Date(), dateFormat); changeTimeAmin(); handler.postDelayed(this, 1000); } }; public TimeView(Context context) { super(context); } public TimeView(Context context, AttributeSet attrs) { super(context, attrs); getStyleAttr(attrs); //初始化当前时间 presentTxt = DateUtil.format(new Date(), dateFormat); //初始化画笔并赋初始值 mPaint = new Paint(); mPaint.setColor(textColor); mPaint.setTextSize(textSize); //初始化文本测量对象 fontMetrics = mPaint.getFontMetrics(); } public TimeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private void getStyleAttr(AttributeSet attrs) { TypedArray array = null; //获取自定义参数的值 try { array = getContext().obtainStyledAttributes(attrs, R.styleable.TimeView); //字体大小 textSize = array.getDimensionPixelSize(R.styleable.TimeView_textSize, 20); //字体颜色 textColor = array.getColor(R.styleable.TimeView_textColor, Color.parseColor("#eeeeee")); //日期显示的样式 dateFormat = FORMAT_VALUES.get(array.getInt(R.styleable.TimeView_format, 6)); } catch (Exception e) { //一旦异常,给定一个默认值 textSize = 20; textColor = Color.parseColor("#eeeeee"); dateFormat = FORMAT_VALUES.get(6); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //测量控件 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); width = 0; height = 0; switch (widthMode) { case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: width = (int) (mPaint.measureText(presentTxt) + getPaddingLeft() + getPaddingRight()); break; case MeasureSpec.UNSPECIFIED: width = getSuggestedMinimumWidth(); break; } switch (heightMode) { case MeasureSpec.EXACTLY: height = heightSize; break; case MeasureSpec.AT_MOST: //自适应的时候,计算出文本的高度加上paddingtop和bottom height = (int) (Math.ceil(fontMetrics.descent - fontMetrics.ascent) + getPaddingTop() + getPaddingBottom()); break; case MeasureSpec.UNSPECIFIED: height = getSuggestedMinimumWidth(); break; } setMeasuredDimension(width, height * 2); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //一开始就开启一个计时器 1秒之后更新时间 handler.postDelayed(timer, 1000); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); presentTime.clear(); newTime.clear(); //将时间的文本拆解成一个单文本的集合,以便于动态的更新单个字符的动画 for (int i = 0; i < presentTxt.length(); i++) { presentTime.add(presentTxt.substring(i, i + 1)); if (null != newTimeTxt && newTimeTxt.length() > 0) newTime.add(newTimeTxt.substring(i, i + 1)); } int leftX = 0; //循环绘制一个个的单字符 for (int i = 0; i < presentTime.size(); i++) { //绘制最左边的时候,将paddingLeft加上去 if (i == 0) leftX += getPaddingLeft(); //如果是第一次绘制的时候,就只有当前时间 没有新的时间 if (null != newTime && newTime.size() > 0) { //如果最新的时间不为空的时候 //单个字符相同,说明单字符对应地方的时间没有变化 //比如20160813164400-->20160813164401,就只需要最后的1需要从0-->1,其他的都不需要动 if (presentTime.get(i).equals(newTime.get(i))) { //这里就是绘制2016081316440这部分不需要有动画的部分 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop(), mPaint); } else { //这里就是绘制最后的那个0-->1的动画 //因为changeTimeAmin()方法中的动画一直在不停的改变rollStep的值并不停的在重绘,一次就产生的动的效果 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop() - rollStep, mPaint); canvas.drawText(newTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop() + height - rollStep, mPaint); } } else //只绘制当前时间的文本 canvas.drawText(presentTime.get(i), leftX, (int) Math.ceil(0 - fontMetrics.ascent) + getPaddingTop(), mPaint); leftX += mPaint.measureText(presentTime.get(i)); } } /** * 更新滚动步长的动画 */ public void changeTimeAmin() { ValueAnimator animator = ValueAnimator.ofInt(0, height); animator.setDuration(800); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { rollStep = (int) animation.getAnimatedValue(); if (rollStep >= height) { presentTxt = newTimeTxt; newTimeTxt = DateUtil.format(new Date(), dateFormat); } else invalidate(); } }); animator.start(); } }

    布局

    <com.lpf.afeidemo.views.timeview.view.TimeView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:background="#eeeeee" android:padding="5dp" app:format="FORMAT_LONG_CN" app:textColor="#000000" app:textSize="15sp" /> 使用自定义属性的时候,记得要在布局最外层加上xmlns:app="http://schemas.android.com/apk/res-auto",代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > </LinearLayout>

    以上是使用的当前时间去切换实现,实际开发过程中,比如商场类的应用,经常也会出现抢购剩余时间的效果(倒计时),和此处的所需要实现的东西差不多,知道原理之后,稍微改吧改吧,就可以用了。

    帮助类

    以上控件中用到了一个日期帮助类,通用的,可以直接使用,代码如下: import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class DateUtil { /** * 英文简写(默认)如:2010-12-01 */ public static String FORMAT_SHORT = "yyyy-MM-dd"; /** * 英文全称 如:2010-12-01 23:15:06 */ public static String FORMAT_LONG = "yyyy-MM-dd HH:mm:ss"; /** * 精确到毫秒的完整时间 如:yyyy-MM-dd HH:mm:ss.S */ public static String FORMAT_FULL = "yyyy-MM-dd HH:mm:ss.S"; /** * 年月日十分秒 不要横线 */ public static String FORMAT_FULL_NOLINE = "yyyyMMddHHmmss"; /** * 年月日 不要横线 */ public static String FORMAT_SHORT_NOLINE = "yyyyMMdd"; /** * 中文简写 如:2010年12月01日 */ public static String FORMAT_SHORT_CN = "yyyy年MM月dd"; /** * 中文全称 如:2010年12月01日 23时15分06秒 */ public static String FORMAT_LONG_CN = "yyyy年MM月dd日 HH时mm分ss秒"; /** * 精确到毫秒的完整中文时间 */ public static String FORMAT_FULL_CN = "yyyy年MM月dd日 HH时mm分ss秒SSS毫秒"; /** * 获得默认的 date pattern */ public static String getDatePattern() { return FORMAT_FULL_NOLINE; } /** * 根据预设格式返回当前日期 * * @return */ public static String getNow() { return format(new Date()); } /** * 根据用户格式返回当前日期 * * @param format * @return */ public static String getNow(String format) { return format(new Date(), format); } /** * 使用预设格式格式化日期 * * @param date * @return */ public static String format(Date date) { return format(date, getDatePattern()); } /** * 使用用户格式格式化日期 * * @param date * 日期 * @param pattern * 日期格式 * @return */ public static String format(Date date, String pattern) { String returnValue = ""; if (date != null) { SimpleDateFormat df = new SimpleDateFormat(pattern); returnValue = df.format(date); } return (returnValue); } /** * 使用预设格式提取字符串日期 * * @param strDate * 日期字符串 * @return */ public static Date parse(String strDate) { return parse(strDate, getDatePattern()); } /** * 使用用户格式提取字符串日期 * * @param strDate * 日期字符串 * @param pattern * 日期格式 * @return */ public static Date parse(String strDate, String pattern) { SimpleDateFormat df = new SimpleDateFormat(pattern); try { return df.parse(strDate); } catch (ParseException e) { e.printStackTrace(); return null; } } public static String parseTo(String strDate) { StringBuffer sb = new StringBuffer(); sb.append(strDate.substring(0, 4)).append("-").append(strDate.substring(4, 6)).append("-").append(strDate.substring(6, 8)).append(" ").append(strDate.substring(8, 10)) .append(":").append(strDate.substring(10, 12)).append(":").append(strDate.substring(12)); return sb.toString(); } /** * 在日期上增加数个整月 * * @param date * 日期 * @param n * 要增加的月数 * @return */ public static Date addMonth(Date date, int n) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.MONTH, n); return cal.getTime(); } /** * 在日期上增加天数 * * @param date * 日期 * @param n * 要增加的天数 * @return */ public static Date addDay(Date date, int n) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.DATE, n); return cal.getTime(); } /** * 获取时间戳 */ public static String getTimeString() { SimpleDateFormat df = new SimpleDateFormat(FORMAT_FULL); Calendar calendar = Calendar.getInstance(); return df.format(calendar.getTime()); } /** * 获取日期年份 * * @param date * 日期 * @return */ public static String getYear(Date date) { return format(date).substring(0, 4); } /** * 按默认格式的字符串距离今天的天数 * * @param date * 日期字符串 * @return */ public static int countDays(String date) { long t = Calendar.getInstance().getTime().getTime(); Calendar c = Calendar.getInstance(); c.setTime(parse(date)); long t1 = c.getTime().getTime(); return (int) (t / 1000 - t1 / 1000) / 3600 / 24; } /** * 按用户格式字符串距离今天的天数 * * @param date * 日期字符串 * @param format * 日期格式 * @return */ public static int countDays(String date, String format) { long t = Calendar.getInstance().getTime().getTime(); Calendar c = Calendar.getInstance(); c.setTime(parse(date, format)); long t1 = c.getTime().getTime(); return (int) (t / 1000 - t1 / 1000) / 3600 / 24; } /** * 获取时间字符串 * * @param hour * @param min * @return */ public static String getTimeStr(int hour, int min) { StringBuilder builder = new StringBuilder(); if (hour < 10) { builder.append(0); } builder.append(hour).append(':'); if (min < 10) { builder.append(0); } builder.append(min); return builder.toString(); } }
    转载请注明原文地址: https://ju.6miu.com/read-1298246.html
    最新回复(0)