android CountDownTimer - дополнительная задержка в миллисекундах между тиками
По моим наблюдениям, countDownTimer для android countDownInterval между тиками оказывается не точным, countDownInterval обычно на несколько миллисекунд дольше, чем указано. CountDownInterval в моем конкретном приложении составляет 1000 мс, просто отсчитывая определенное время с шагом в одну секунду.
Из-за этих длительных тиков у меня останется меньше тиков, чем хотелось бы, когда отсчет времени работает достаточно долго, что приводит к появлению обратного отсчета времени (2-секундный шаг происходит на уровне пользовательского интерфейса, когда суммируется достаточно дополнительных мс)
Глядя на источник CountDownTimer, кажется, что его можно исказить, чтобы он исправил эту нежелательную неточность, но мне было интересно, есть ли уже лучший CountDownTimer, доступный в мире java/android.
Спасибо приятели за любой указатель...
5 ответов
перезапись
Как вы сказали, вы также заметили, что в следующий раз в onTick()
рассчитывается от времени предыдущего onTick()
побежал, что вносит крошечную ошибку на каждом тике. Я изменил исходный код CountDownTimer для вызова каждого onTick()
через указанные промежутки времени от начала.
Я построил это на платформе CountDownTimer, поэтому вырезайте и вставляйте исходный код в свой проект и присваивайте классу уникальное имя. (Я назвал мой MoreAccurateTimer.) Теперь внесите несколько изменений:
Добавьте новую переменную класса:
private long mNextTime;
+ Изменить
start()
:public synchronized final MoreAccurateTimer start() { if (mMillisInFuture <= 0) { onFinish(); return this; } mNextTime = SystemClock.uptimeMillis(); mStopTimeInFuture = mNextTime + mMillisInFuture; mNextTime += mCountdownInterval; mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG), mNextTime); return this; }
Изменить хендлера
handlerMessage()
:@Override public void handleMessage(Message msg) { synchronized (MoreAccurateTimer.this) { final long millisLeft = mStopTimeInFuture - SystemClock.uptimeMillis(); if (millisLeft <= 0) { onFinish(); } else { onTick(millisLeft); // Calculate next tick by adding the countdown interval from the original start time // If user's onTick() took too long, skip the intervals that were already missed long currentTime = SystemClock.uptimeMillis(); do { mNextTime += mCountdownInterval; } while (currentTime > mNextTime); // Make sure this interval doesn't exceed the stop time if(mNextTime < mStopTimeInFuture) sendMessageAtTime(obtainMessage(MSG), mNextTime); else sendMessageAtTime(obtainMessage(MSG), mStopTimeInFuture); } } }
Так вот что я придумал. Это небольшая модификация оригинального CountDownTimer. Он добавляет переменную mTickCounter, которая подсчитывает количество вызванных тиков. Эта переменная используется вместе с новой переменной mStartTime, чтобы увидеть, насколько мы точны с нашими тиками. Основываясь на этом входе, корректируется задержка до следующего тика... Кажется, это делает то, что я искал, но я уверен, что это можно улучшить.
Ищу
// ************AccurateCountdownTimer***************
в исходном коде, чтобы найти модификации, которые я добавил в исходный класс.
package com.dorjeduck.xyz;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
/**
* Schedule a countdown until a time in the future, with regular notifications
* on intervals along the way.
*
* Example of showing a 30 second countdown in a text field:
*
* <pre class="prettyprint">
* new CountDownTimer(30000, 1000) {
*
* public void onTick(long millisUntilFinished) {
* mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
* }
*
* public void onFinish() {
* mTextField.setText("done!");
* }
* }.start();
* </pre>
*
* The calls to {@link #onTick(long)} are synchronized to this object so that
* one call to {@link #onTick(long)} won't ever occur before the previous
* callback is complete. This is only relevant when the implementation of
* {@link #onTick(long)} takes an amount of time to execute that is significant
* compared to the countdown interval.
*/
public abstract class AccurateCountDownTimer {
/**
* Millis since epoch when alarm should stop.
*/
private final long mMillisInFuture;
/**
* The interval in millis that the user receives callbacks
*/
private final long mCountdownInterval;
private long mStopTimeInFuture;
// ************AccurateCountdownTimer***************
private int mTickCounter;
private long mStartTime;
// ************AccurateCountdownTimer***************
/**
* @param millisInFuture
* The number of millis in the future from the call to
* {@link #start()} until the countdown is done and
* {@link #onFinish()} is called.
* @param countDownInterval
* The interval along the way to receive {@link #onTick(long)}
* callbacks.
*/
public AccurateCountDownTimer(long millisInFuture, long countDownInterval) {
mMillisInFuture = millisInFuture;
mCountdownInterval = countDownInterval;
// ************AccurateCountdownTimer***************
mTickCounter = 0;
// ************AccurateCountdownTimer***************
}
/**
* Cancel the countdown.
*/
public final void cancel() {
mHandler.removeMessages(MSG);
}
/**
* Start the countdown.
*/
public synchronized final AccurateCountDownTimer start() {
if (mMillisInFuture <= 0) {
onFinish();
return this;
}
// ************AccurateCountdownTimer***************
mStartTime = SystemClock.elapsedRealtime();
mStopTimeInFuture = mStartTime + mMillisInFuture;
// ************AccurateCountdownTimer***************
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return this;
}
/**
* Callback fired on regular interval.
*
* @param millisUntilFinished
* The amount of time until finished.
*/
public abstract void onTick(long millisUntilFinished);
/**
* Callback fired when the time is up.
*/
public abstract void onFinish();
private static final int MSG = 1;
// handles counting down
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (AccurateCountDownTimer.this) {
final long millisLeft = mStopTimeInFuture
- SystemClock.elapsedRealtime();
if (millisLeft <= 0) {
onFinish();
} else if (millisLeft < mCountdownInterval) {
// no tick, just delay until done
sendMessageDelayed(obtainMessage(MSG), millisLeft);
} else {
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
// ************AccurateCountdownTimer***************
long now = SystemClock.elapsedRealtime();
long extraDelay = now - mStartTime - mTickCounter
* mCountdownInterval;
mTickCounter++;
long delay = lastTickStart + mCountdownInterval - now
- extraDelay;
// ************AccurateCountdownTimer***************
// take into account user's onTick taking time to execute
// special case: user's onTick took more than interval to
// complete, skip to next interval
while (delay < 0)
delay += mCountdownInterval;
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};
}
В большинстве систем таймеры никогда не бывают абсолютно точными. Система выполняет таймер только тогда, когда ей больше нечего делать. Если ЦП занят фоновым процессом или другим потоком, он не сможет вызвать таймер, пока не завершит работу.
Возможно, вам повезет, изменив интервал на что-то меньшее, например, 100 мс, а затем перерисовать экран, только если что-то изменилось. При таком подходе таймер напрямую не вызывает ничего, он просто периодически перерисовывает экран.
Я знаю, что он старый, но это то, что тебе всегда будет нужно. Я написал класс под названием TimedTaskExecutor, потому что мне нужно было выполнять определенную команду каждые x миллисекунд в течение неизвестного промежутка времени. Одним из моих главных приоритетов было сделать х максимально точным. Я пытался с AsyncTask, Handlers и CountdownTimer, но все это дало мне плохие результаты. Вот класс:
package com.example.myapp;
import java.util.concurrent.TimeUnit;
/*
* MUST RUN IN BACKGROUND THREAD
*/
public class TimedTaskExecutor {
// ------------------------------ FIELDS ------------------------------
/**
*
*/
private double intervalInMilliseconds;
/**
*
*/
private IVoidEmptyCallback callback;
/**
*
*/
private long sleepInterval;
// --------------------------- CONSTRUCTORS ---------------------------
/**
* @param intervalInMilliseconds
* @param callback
*/
public TimedTaskExecutor(double intervalInMilliseconds, IVoidEmptyCallback callback, long sleepInterval) {
this.intervalInMilliseconds = intervalInMilliseconds;
this.callback = callback;
this.sleepInterval = sleepInterval;
}
// --------------------- GETTER / SETTER METHODS ---------------------
/**
* @return
*/
private IVoidEmptyCallback getCallback() {
return callback;
}
/**
* @return
*/
private double getIntervalInMilliseconds() {
return intervalInMilliseconds;
}
/**
* @return
*/
private long getSleepInterval() {
return sleepInterval;
}
// -------------------------- OTHER METHODS --------------------------
/**
*
*/
public void run(ICallback<Boolean> isRunningChecker) {
long nanosInterval = (long) (getIntervalInMilliseconds() * 1000000);
Long previousNanos = null;
while (isRunningChecker.callback()) {
long nanos = TimeUnit.NANOSECONDS.toNanos(System.nanoTime());
if (previousNanos == null || (double) (nanos - previousNanos) >= nanosInterval) {
getCallback().callback();
if (previousNanos != null) {
// Removing the difference
previousNanos = nanos - (nanos - previousNanos - nanosInterval);
} else {
previousNanos = nanos;
}
}
if (getSleepInterval() > 0) {
try {
Thread.sleep(getSleepInterval());
} catch (InterruptedException ignore) {
}
}
}
}
// -------------------------- INNER CLASSES --------------------------
/**
*
*/
public interface IVoidEmptyCallback {
/**
*
*/
public void callback();
}
/**
* @param <T>
*/
public interface ICallback<T> {
/**
* @return
*/
public T callback();
}
}
Вот пример того, как его использовать:
private boolean running;
Handler handler = new Handler();
handler.postDelayed(
new Runnable() {
/**
*
*/
@Override
public void run() {
running = false;
}
},
5000
);
HandlerThread handlerThread = new HandlerThread("For background");
handlerThread.start();
Handler background = new Handler(handlerThread.getLooper());
background.post(
new Runnable() {
/**
*
*/
@Override
public void run() {
new TimedTaskExecutor(
10, // Run tick every 10 milliseconds
// The callback for each tick
new TimedTaskExecutor.IVoidEmptyCallback() {
/**
*
*/
private int counter = 1;
/**
*
*/
@Override
public void callback() {
// You can use the handler to post runnables to the UI
Log.d("runTimedTask", String.valueOf(counter++));
}
},
// sleep interval in order to allow the CPU to rest
2
).run(
// A callback to check when to stop
new TimedTaskExecutor.ICallback<Boolean>() {
/**
*
* @return
*/
@Override
public Boolean callback() {
return running;
}
}
);
}
}
);
Выполнение этого кода даст 500 вызовов с более или менее точным значением x. (снижение коэффициента сна делает его более точным)
- Изменить: Кажется, что в Nexus 5 с Lollipop вы должны использовать 0 в фактор сна.
Как ранее писал Натан Вильяэскуса, хорошим подходом является уменьшение "тикающего" интервала, а затем выполнение необходимых ДЕЙСТВИЙ каждую секунду, проверяя, действительно ли это новая секунда.
Ребята, взгляните на такой подход:
new CountDownTimer(5000, 100) {
int n = 5;
Toast toast = null;
@Override
public void onTick(long l) {
if(l < n*1000) {
//
// new seconds. YOUR ACTIONS
//
if(toast != null) toast.cancel();
toast = Toast.makeText(MainActivity.this, "" + n, Toast.LENGTH_SHORT);
toast.show();
// this one is important!!!
n-=1;
}
}
@Override
public void onFinish() {
if(toast != null) toast.cancel();
Toast.makeText(MainActivity.this, "START", Toast.LENGTH_SHORT).show();
}
}.start();
Надеюсь, это кому-то поможет;p
Я написал одну библиотеку, чтобы предотвратить это явление.
https://github.com/imknown/NoDelayCountDownTimer
Основные коды для использования:
private long howLongLeftInMilliSecond = NoDelayCountDownTimer.SIXTY_SECONDS;
private NoDelayCountDownTimer noDelayCountDownTimer;
private TextView noDelayCountDownTimerTv;
NoDelayCountDownTimer noDelayCountDownTimer = new NoDelayCountDownTimerInjector<TextView>(noDelayCountDownTimerTv, howLongLeftInMilliSecond).inject(new NoDelayCountDownTimerInjector.ICountDownTimerCallback() {
@Override
public void onTick(long howLongLeft, String howLongSecondLeftInStringFormat) {
String result = getString(R.string.no_delay_count_down_timer, howLongSecondLeftInStringFormat);
noDelayCountDownTimerTv.setText(result);
}
@Override
public void onFinish() {
noDelayCountDownTimerTv.setText(R.string.finishing_counting_down);
}
});
Основные базовые логические коды:
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
synchronized (NoDelayCountDownTimer.this) {
if (mCancelled) {
return true;
}
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
if (millisLeft <= 0 || millisLeft < mCountdownInterval) {
onFinish();
} else {
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
// take into account user's onTick taking time to execute
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
// special case: user's onTick took more than interval to complete, skip to next interval
while (delay < 0) {
delay += mCountdownInterval;
}
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG), delay);
}
}
return true;
}
});