闲来无聊,写了一个带动画的日期时间控件,分享一下,具体效果如下:
原理 原理挺简单,拿到当前时间和下一秒的最新时间,当前时间显示在课件位置,新的时间绘制在下方不可见的区域;因为涉及到单字符的动画,所以,在时间绘制的时候,我们将“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>以上是使用的当前时间去切换实现,实际开发过程中,比如商场类的应用,经常也会出现抢购剩余时间的效果(倒计时),和此处的所需要实现的东西差不多,知道原理之后,稍微改吧改吧,就可以用了。