在很多电商工作项目中经常有倒计时的场景,比如活动倒计时、抢红包倒计时等等,今天小编就带大家来学习如何利用Android设计倒计时组件,感兴趣的小伙伴一起奥
那么作为倒计时使用会有什么问题呢?
问题一是回调可能不准确,假设倒计时9.5秒,每1秒刷新一次view,该怎么设置回调间隔时间呢?
问题二是在手机长时间息屏后,某些厂商会将CPU休眠,RxJava的interval操作符此时将被按下暂停键,当APP再次回到前台,interval会继续执行,假设暂停时倒计时剩余100秒,回到前台后实际只有10秒了,但是interval还是从100继续执行。
2.2 支持多任务
Timer是单线程串行执行多任务,假设taskA设定1秒后执行,taskB设定2秒后执行,实际上taskB是在taskA执行结束后才执行taskB,所以taskB的执行时间是在第3秒,所以Timer只算是伪支持多任务。ScheduledExecutorService是利用线程池支持了多任务调度的。
2.3 支持时间校准
CountDownTimer中每次onTick()方法回调,都会重新计算下一次onTick的时间。其中主要优化有2点,一是减去onTick执行耗时;二是针对特殊情况(如1.2.1中提到的手机息屏后CPU休眠场景),对比delay是否小于0,如果小于0则需要累加mCountdownInterval。
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart;
long delay;
if (millisLeft < mCountdownInterval) {
// 减去上面onTick方法执行耗时
delay = millisLeft - lastTickDuration;
if (delay < 0) {
delay = 0;
} else {
delay = mCountdownInterval - lastTickDuration;
// 针对特殊情况(如1.2.1中提到的手机息屏后CPU休眠场景)
// 对比delay是否小于0,如果小于0则需要累加mCountdownInterval
while (delay < 0) {
delay += mCountdownInterval;
}
}
sendMessageDelayed(obtainMessage(MSG), delay);
}
2.4 支持同帧刷新
我们项目中有很多场景是这样的:
倒计时A先执行,倒计时B后执行,A和B的倒计时结束时间是一致的,那么我们假设倒计时时间为10秒,每1秒刷新一次,A在剩余10秒时执行,B在剩余9.5秒执行,当二者在同一页面显示时,就会刷新不一致,这个问题在我们新的倒计时组件中将得到解决,文章后面将会详细说明。
2.5 支持延迟执行
延迟1分钟再执行10秒的倒计时?Android中提供的CountDownTimer是做不到的,只能额外写一个1分钟的定时器,到时间后再启动倒计时。
2.6 支持CPU休眠
我们这里提到的支持CPU休眠,并不是指CPU休眠期间倒计时仍能得到执行,而是在CPU休眠后能够恢复正常执行。和1.2.3中提到的时间校准类似,解决了时间校准的问题也就支持了CPU休眠的特性。
3 需求目标
- 设计一个中心化的倒计时组件,同时支持上述提到的一系列特性。
- 接口易于调用,使用者只需关注计时回调的逻辑。
4 设计类结构
CountDownTimer采用静态内部类形式实现单例,暴露countdown() 、timer()方法供业务方ClientA/ClientB/ClientC等调用,Task是抽象任务,每次调用countdown() 、timer()后都生成一个task,交给优先级队列管理,内部通过handler不断从队列中取task执行。
5 具体实现
5.1 收口
收口可以理解为进行统一管理,这里我们通过一个优先级队列管理所有倒计时、定时器,优先级队列可以直接采用Java中已有的数据结构PriorityQueue,设置队列大小默认为5,根据task中的mExecuteTimeInNext进行正序排序。这里有一个特别需要注意的点,PriorityQueue需要传入实现Comparator接口的对象,在实现Comparator时,因为mExecuteTimeInNext的数据类型是long类型,而compare()方法返回的是int类型,如果直接使用二者相减再强制转换为int,会有溢出的风险,所以可以使用Long.compare()来实现大小比较。
/**
* 优先级队列,保存task,以 {@link Task#mExecuteTimeInNext} 作为基准
*/
private final Queue<Task> mTaskQueue = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY,
new Comparator<Task>() {
@Override
public int compare(Task task1, Task task2) {
// return (int) (task1.mExecuteTimeInNext - task2.mExecuteTimeInNext); 错误示范
return Long.compare(task1.mExecuteTimeInNext, task2.mExecuteTimeInNext);
}
});
5.2 支持与RxJava协同
提供倒计时countdown、定时器timer操作符,直接返回Observable,方便与RxJava框架协同。
/**
* 倒计时
*
* @param millisInFuture Millis since epoch when alarm should stop.
* @param countDownInterval The interval in millis that the user receives callbacks.
* @param delayMillis The delay time in millis.
* @return Observable
*/
public synchronized Observable<Long> countdown(long millisInFuture, long countDownInterval, long delayMillis) {
AtomicReference<Task> taskAtomicReference = new AtomicReference<>();
return Observable.create((ObservableOnSubscribe<Long>) emitter -> {
Task newTask = new Task(millisInFuture, countDownInterval, delayMillis, emitter);
taskAtomicReference.set(newTask);
synchronized (CountDownTimerManager.this) {
Task topTask = mTaskQueue.peek();
if (topTask == null || newTask.mExecuteTimeInNext < topTask.mExecuteTimeInNext) {
cancel();
}
mTaskQueue.offer(newTask);
if (mCancelled) {
start();
}
}
}).doOnDispose(() -> {
if (taskAtomicReference.get() != null) {
taskAtomicReference.get().dispose();
}
});
}
/**
* 定时器
*
* @param millisInFuture Millis since epoch when alarm should stop.
* @return Observable
*/
public synchronized Observable<Long> timer(long millisInFuture) {
return countdown(0, 0, millisInFuture);
}
private synchronized void remove(Task task) {
mTaskQueue.remove(task);
if (mTaskQueue.size() == 0) {
cancel();
}
}
5.3 支持时间校准
不推荐使用RxJava中的interval,因为RxJava中的实现无法保障倒计时的准确执行,如在手机CPU进入休眠之后再恢复到前台。那么如何实现呢?这里借鉴了Android中CountDownTimer的设计思路,在每次onTick后重新计算了下一次onTick的时间,比如前文提到的“CPU进入休眠”的情况,我们通过一个while循环,计算出下一次onTick的时间(其条件是大于当前时间)。
mTaskQueue.poll();
if (!task.isDisposed()) {
if (stopMillisLeft <= 0 || task.mCountdownInterval == 0) {
task.mDisposed = true;
task.mEmitter.onNext(0L);
task.mEmitter.onComplete();
} else {
task.mEmitter.onNext(stopMillisLeft % task.mCountdownInterval == 0 ? stopMillisLeft
: (stopMillisLeft / task.mCountdownInterval + 1) * task.mCountdownInterval);
// 时间校准
// special case:
// user's onTick took more than interval to complete
// cpu slept
do {
task.mExecuteTimeInNext += task.mCountdownInterval;
} while (task.mExecuteTimeInNext < SystemClock.elapsedRealtime());
mTaskQueue.offer(task);
}
}
5.4 支持同步刷新
针对多个倒计时在同一时刻结束的情况,优化了刷新不同步的问题。 mExecuteTimeInNext是下一次任务执行时间,假设倒计时剩余时间为9.5秒,每1秒刷新,那么下一次的执行时间则是在0.5秒之后。
private Task(long millisInFuture, long countDownInterval, long delayMillis,
@NonNull ObservableEmitter<Long> emitter) {
mCountdownInterval = countDownInterval;
// 计算出下次执行的时间
mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
: millisInFuture % mCountdownInterval) + delayMillis;
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
mEmitter = emitter;
}
5.5 支持延迟执行
在计算下次执行的时间时,加上了delayMillis,这样就支持了延迟执行。
private Task(long millisInFuture, long countDownInterval, long delayMillis,
@NonNull ObservableEmitter<Long> emitter) {
mCountdownInterval = countDownInterval;
// 计算出下次执行的时间
mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
: millisInFuture % mCountdownInterval) + delayMillis;
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
mEmitter = emitter;
}
到此这篇关于利用Android设计一个倒计时组件的文章就介绍到这了,更多相关利用Android设计倒计时组件内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!
本文标题为:利用Android设计一个倒计时组件
- Android MaterialButton使用实例详解(告别shape、selector) 2023-06-16
- Android实现轮询的三种方式 2023-02-17
- Android实现监听音量的变化 2023-03-30
- Android studio实现动态背景页面 2023-05-23
- iOS 对当前webView进行截屏的方法 2023-03-01
- 最好用的ios数据恢复软件:PhoneRescue for Mac 2023-09-14
- 作为iOS开发,这道面试题你能答出来,说明你基础很OK! 2023-09-14
- SurfaceView播放视频发送弹幕并实现滚动歌词 2023-01-02
- Flutter实现底部和顶部导航栏 2022-08-31
- 详解flutter engine 那些没被释放的东西 2022-12-04
