利用Android设计一个倒计时组件
投稿:zhanglei
1 背景
我们在项目中经常有倒计时的场景,比如活动倒计时、抢红包倒计时等等。通常情况下,我们实现倒计时的方案有Android
中的CountDownTimer
、Java
中自带的Timer
和ScheduleExcutorService
、RxJava
中的interval
操作符。 在实际项目中存在2个典型的问题,一是倒计时的实现形式不统一,不统一的原因分为认知不一致、每种倒计时方案各有优势;二是存在大量倒计时同时执行。
2 对比分析
关于几种方案的用法不是本文要讨论的重点,在此我们通过表格的方式列出来各自的特性,表格底部的CountDownTimerManager
就是本文要为大家介绍的新鲜出炉的中心化倒计时组件。
2.1 是否是倒计时
Rx中的interval
操作符是每隔一段时间会发送一个事件,可以说是一个计数器,而不是倒计时,在实际项目中会发现很多同学都把它当做倒计时在使用。下图是RxJava
官方对interval
的图解:
interval.png *The Interval operator returns an Observable that emits an infinite sequence of ascending integers, with a constant interval of time of your choosing between emissions.(简单理解就是固定间隔时间进行回调)
通过源码,我们也可以看出在ObservableInterval
中实际也是进行了周期性调度。
public final class ObservableInterval extends Observable<Long> { @Override public void subscribeActual(Observer<? super Long> observer) { IntervalObserver is = new IntervalObserver(observer); observer.onSubscribe(is); Scheduler sch = scheduler; if (sch instanceof TrampolineScheduler) { Worker worker = sch.createWorker(); is.setResource(worker); // 以给定的初始时间延迟、周期时间进行周期性执行 worker.schedulePeriodically(is, initialDelay, period, unit); } else { // 以给定的初始时间延迟、周期时间进行周期性执行 Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); is.setResource(d); } }
那么作为倒计时使用会有什么问题呢?
问题一是回调可能不准确,假设倒计时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设计倒计时组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!