公司项目中有大量的倒计时,刚好看到了快手电商无限团队的 一起设计一个Android倒计时组件 这篇文章感觉甚好,没有开源出来,评论区掘友们也在求,所以开源一下,致敬快手电商团队分享知识。
测试过程与效果
分别开启三个间隔为500L、2000L、1000L的倒计时,测试时间为 10 * 1000L。
manager.countdown(millisInFuture, 500L, 0L).subscribe(new Observer<Long>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.d("CountDownTimer1", "test countdown");
}
@Override
public void onNext(@NonNull Long value) {
Log.d("CountDownTimer1", "countdown Remaining time1: " + value);
}
@Override
public void onComplete() {
Log.d("CountDownTimer1", "countdown end1");
}
});
manager.countdown(millisInFuture, 2000L, 0L).subscribe(new Observer<Long>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.d("CountDownTimer2", "test countdown");
}
@Override
public void onNext(@NonNull Long value) {
Log.d("CountDownTimer2", "countdown Remaining time2: " + value);
}
@Override
public void onComplete() {
Log.d("CountDownTimer2", "countdown end2");
}
});
manager.countdown(millisInFuture, 1000L, 0L).subscribe(new Observer<Long>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.d("CountDownTimer3", "test countdown");
}
@Override
public void onNext(@NonNull Long value) {
Log.d("CountDownTimer3", "countdown Remaining time3: " + value);
}
@Override
public void onComplete() {
Log.d("CountDownTimer3", "countdown end3");
}
});
目前来看应该(大概)修复了长时间定时存在偏差的原因,之前写的版本跑一个小时,中间的时间损耗会呈现一个周期变化,直到大于每次的间隔时间;现在这个版本跑一个小时没有问题,解决时间损耗,能把精度控制在
5ms内,基于 Handler 机制设计是这样的。官方的 CountDownTimer 也是基于 Handler 机制,Handler 机制中的 Message 的触发时间只是表示该 Message 会在大于等于触发时间的时机触发。
实现过程
有试过用 ChatGPT 来复现,结果给的代码一大堆 Bug。
在自己实现过程中也碰到了问题,感谢 奶盖配红茶 大佬指点。
至于暂停和恢复功能,有兴趣的朋友可以自己加上。
Kotlin 版本的话用 SharedFlow 和 StateFlow 改造都可以。
源码设计
具体设计与思想请看快手文章,上面有链接。
dependencies {
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
}
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicReference;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
public class CountDownTimerManager {
private CountDownTimerManager() {}
public static CountDownTimerManager getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final CountDownTimerManager sInstance = new CountDownTimerManager();
}
private static final Integer DEFAULT_INITIAL_CAPACITY = 5;
private static final int MSG = 1;
private boolean mIsCancelled = false;
/**
* 任务队列,按照 {@link Task#mExecuteTimeInNext} 时间排序
*/
private Comparator<? super Task> getComparator() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Comparator.comparingLong((Task o) -> o.mExecuteTimeInNext);
}
return (Comparator<Task>) (o1, o2) -> Long.compare(o1.mExecuteTimeInNext,o2.mExecuteTimeInNext);
}
private final PriorityQueue<Task> mTaskQueue = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY, getComparator());
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
synchronized (CountDownTimerManager.this) {
if (mIsCancelled) {
return;
}
Task task = mTaskQueue.poll();
if (task != null && !task.isDisposed()) {
if (task.mMillisUntilFinished < task.mCountdownInterval || task.mCountdownInterval == 0) {
task.dispose();
} else {
task.mEmitter.onNext(task.mMillisUntilFinished);
// 更新剩余时间
task.mMillisUntilFinished = task.mMillisUntilFinished - task.mCountdownInterval;
// 更新下一次执行时间
task.mExecuteTimeInNext += task.mCountdownInterval;
// 下一个 MSG 执行时间
long mNextMsgCountDownInterval = task.mExecuteTimeInNext - SystemClock.elapsedRealtime();
while (mNextMsgCountDownInterval < 0) {
mNextMsgCountDownInterval += task.mCountdownInterval;
}
long diff = Math.abs(mNextMsgCountDownInterval - task.mCountdownInterval);
if(diff >= 10) {
Log.d("CountDownTimerManager","下一次触发时间 偏差大于10:" + mNextMsgCountDownInterval);
}
mTaskQueue.offer(task);
final Message nextMsg = mHandler.obtainMessage(MSG);
mHandler.sendMessageDelayed(nextMsg, Math.max(10, mNextMsgCountDownInterval));
}
}
}
}
};
public synchronized Observable<Long> countdown(long millisInFuture, long countDownInterval, long delayMillis) {
AtomicReference<Task> taskAtomicReference = new AtomicReference<>();
return Observable.create((ObservableOnSubscribe<Long>) emitter -> {
if (millisInFuture <= 0) {
emitter.onError(new IllegalArgumentException("millisInFuture must be greater than 0"));
}
if (countDownInterval < 0) {
emitter.onError(new IllegalArgumentException("countDownInterval must be greater than or equal to 0"));
}
Task newTask = new Task(millisInFuture, countDownInterval, delayMillis, emitter);
Task topTask = mTaskQueue.peek();
if (topTask == null || newTask.mExecuteTimeInNext < topTask.mExecuteTimeInNext) {
cancel();
}
mTaskQueue.offer(newTask);
mIsCancelled = false;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
}).doOnDispose(() -> {
if (taskAtomicReference.get() != null) {
taskAtomicReference.get().dispose();
}
});
}
public synchronized Observable<Long> timer(long delayMillis) {
return countdown(0,0, delayMillis);
}
public synchronized void cancel() {
mIsCancelled = true;
mHandler.removeMessages(MSG);
}
private static class Task {
// 是否结束
boolean mDisposed;
// 剩余时间
long mMillisUntilFinished;
// 间隔时间
long mCountdownInterval;
// 下次执行时间
long mExecuteTimeInNext;
// 结束时间
long mStopTimeInFuture;
ObservableEmitter<Long> mEmitter;
Task(long millisInFuture, long countDownInterval, long delayMillis, @NonNull ObservableEmitter<Long> emitter) {
mMillisUntilFinished = millisInFuture;
mCountdownInterval = countDownInterval;
mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
: millisInFuture % mCountdownInterval) + delayMillis;
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
mEmitter = emitter;
}
void dispose() {
if (!mDisposed && mEmitter != null) {
mEmitter.onComplete();
mEmitter = null;
mDisposed = true;
}
}
boolean isDisposed() {
return mDisposed || mEmitter == null || mEmitter.isDisposed();
}
}
}