利用Android設計一個倒計時組件
1 背景
我們在項目中經(jīng)常有倒計時的場景,比如活動倒計時、搶紅包倒計時等等。通常情況下,我們實現(xiàn)倒計時的方案有Android
中的CountDownTimer
、Java
中自帶的Timer
和ScheduleExcutorService
、RxJava
中的interval
操作符。 在實際項目中存在2個典型的問題,一是倒計時的實現(xiàn)形式不統(tǒng)一,不統(tǒng)一的原因分為認知不一致、每種倒計時方案各有優(yōu)勢;二是存在大量倒計時同時執(zhí)行。
2 對比分析
關于幾種方案的用法不是本文要討論的重點,在此我們通過表格的方式列出來各自的特性,表格底部的CountDownTimerManager
就是本文要為大家介紹的新鮮出爐的中心化倒計時組件。
2.1 是否是倒計時
Rx中的interval
操作符是每隔一段時間會發(fā)送一個事件,可以說是一個計數(shù)器,而不是倒計時,在實際項目中會發(fā)現(xiàn)很多同學都把它當做倒計時在使用。下圖是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.(簡單理解就是固定間隔時間進行回調(diào))
通過源碼,我們也可以看出在ObservableInterval
中實際也是進行了周期性調(diào)度。
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); // 以給定的初始時間延遲、周期時間進行周期性執(zhí)行 worker.schedulePeriodically(is, initialDelay, period, unit); } else { // 以給定的初始時間延遲、周期時間進行周期性執(zhí)行 Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); is.setResource(d); } }
那么作為倒計時使用會有什么問題呢?
問題一是回調(diào)可能不準確,假設倒計時9.5秒,每1秒刷新一次view,該怎么設置回調(diào)間隔時間呢?
問題二是在手機長時間息屏后,某些廠商會將CPU休眠,RxJava
的interval
操作符此時將被按下暫停鍵,當APP再次回到前臺,interval會繼續(xù)執(zhí)行,假設暫停時倒計時剩余100秒,回到前臺后實際只有10秒了,但是interval
還是從100繼續(xù)執(zhí)行。
2.2 支持多任務
Timer
是單線程串行執(zhí)行多任務,假設taskA設定1秒后執(zhí)行,taskB設定2秒后執(zhí)行,實際上taskB是在taskA執(zhí)行結(jié)束后才執(zhí)行taskB,所以taskB的執(zhí)行時間是在第3秒,所以Timer
只算是偽支持多任務。ScheduledExecutorService
是利用線程池支持了多任務調(diào)度的。
2.3 支持時間校準
CountDownTimer
中每次onTick()方法回調(diào),都會重新計算下一次onTick
的時間。其中主要優(yōu)化有2點,一是減去onTick執(zhí)行耗時;二是針對特殊情況(如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方法執(zhí)行耗時 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先執(zhí)行,倒計時B后執(zhí)行,A和B的倒計時結(jié)束時間是一致的,那么我們假設倒計時時間為10秒,每1秒刷新一次,A在剩余10秒時執(zhí)行,B在剩余9.5秒執(zhí)行,當二者在同一頁面顯示時,就會刷新不一致,這個問題在我們新的倒計時組件中將得到解決,文章后面將會詳細說明。
2.5 支持延遲執(zhí)行
延遲1分鐘再執(zhí)行10秒的倒計時?Android
中提供的CountDownTimer
是做不到的,只能額外寫一個1分鐘的定時器,到時間后再啟動倒計時。
2.6 支持CPU休眠
我們這里提到的支持CPU休眠,并不是指CPU休眠期間倒計時仍能得到執(zhí)行,而是在CPU休眠后能夠恢復正常執(zhí)行。和1.2.3中提到的時間校準類似,解決了時間校準的問題也就支持了CPU休眠的特性。
3 需求目標
- 設計一個中心化的倒計時組件,同時支持上述提到的一系列特性。
- 接口易于調(diào)用,使用者只需關注計時回調(diào)的邏輯。
4 設計類結(jié)構(gòu)
CountDownTimer
采用靜態(tài)內(nèi)部類形式實現(xiàn)單例,暴露countdown()
、timer()
方法供業(yè)務方ClientA/ClientB/ClientC
等調(diào)用,Task是抽象任務,每次調(diào)用countdown()
、timer()
后都生成一個task,交給優(yōu)先級隊列管理,內(nèi)部通過handler不斷從隊列中取task執(zhí)行。
5 具體實現(xiàn)
5.1 收口
收口可以理解為進行統(tǒng)一管理,這里我們通過一個優(yōu)先級隊列管理所有倒計時、定時器,優(yōu)先級隊列可以直接采用Java中已有的數(shù)據(jù)結(jié)構(gòu)PriorityQueue
,設置隊列大小默認為5,根據(jù)task中的mExecuteTimeInNext
進行正序排序。這里有一個特別需要注意的點,PriorityQueue
需要傳入實現(xiàn)Comparator
接口的對象,在實現(xiàn)Comparator
時,因為mExecuteTimeInNext
的數(shù)據(jù)類型是long類型,而compare()
方法返回的是int類型,如果直接使用二者相減再強制轉(zhuǎn)換為int
,會有溢出的風險,所以可以使用Long.compare()
來實現(xiàn)大小比較。
/** * 優(yōu)先級隊列,保存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協(xié)同
提供倒計時countdown
、定時器timer
操作符,直接返回Observable
,方便與RxJava
框架協(xié)同。
/** * 倒計時 * * @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中的實現(xiàn)無法保障倒計時的準確執(zhí)行,如在手機CPU進入休眠之后再恢復到前臺。那么如何實現(xiàn)呢?這里借鑒了Android
中CountDownTimer
的設計思路,在每次onTick后重新計算了下一次onTick的時間,比如前文提到的“CPU進入休眠”的情況,我們通過一個while循環(huán),計算出下一次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 支持同步刷新
針對多個倒計時在同一時刻結(jié)束的情況,優(yōu)化了刷新不同步的問題。 mExecuteTimeInNext
是下一次任務執(zhí)行時間,假設倒計時剩余時間為9.5秒,每1秒刷新,那么下一次的執(zhí)行時間則是在0.5秒之后。
private Task(long millisInFuture, long countDownInterval, long delayMillis, @NonNull ObservableEmitter<Long> emitter) { mCountdownInterval = countDownInterval; // 計算出下次執(zhí)行的時間 mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0 : millisInFuture % mCountdownInterval) + delayMillis; mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis; mEmitter = emitter; }
5.5 支持延遲執(zhí)行
在計算下次執(zhí)行的時間時,加上了delayMillis
,這樣就支持了延遲執(zhí)行。
private Task(long millisInFuture, long countDownInterval, long delayMillis, @NonNull ObservableEmitter<Long> emitter) { mCountdownInterval = countDownInterval; // 計算出下次執(zhí)行的時間 mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0 : millisInFuture % mCountdownInterval) + delayMillis; mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis; mEmitter = emitter; }
到此這篇關于利用Android設計一個倒計時組件的文章就介紹到這了,更多相關利用Android設計倒計時組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android自定義View實現(xiàn)隨機數(shù)驗證碼
這篇文章主要為大家詳細介紹了Android如何利用自定義View實現(xiàn)隨機數(shù)驗證碼效果,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2022-06-06android全屏去掉title欄的多種實現(xiàn)方法
android全屏去掉title欄包括以下幾個部分:實現(xiàn)應用中的所有activity都全屏/實現(xiàn)單個activity全屏/實現(xiàn)單個activity去掉title欄/自定義標題內(nèi)容/自定義標題布局等等感興趣的可參考下啊2013-02-02Android進階從字節(jié)碼插樁技術了解美團熱修復實例詳解
這篇文章主要為大家介紹了Android進階從字節(jié)碼插樁技術了解美團熱修復實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01Android開發(fā)之TextView使用intent傳遞信息,實現(xiàn)注冊界面功能示例
這篇文章主要介紹了Android開發(fā)之TextView使用intent傳遞信息,實現(xiàn)注冊界面功能,涉及Android使用intent傳值及界面布局等相關操作技巧,需要的朋友可以參考下2019-04-04