Android: CountDownTimer se salta el último en Tick ()!

Código:

public class SMH extends Activity { public void onCreate(Bundle b) { super.onCreate(b); setContentView(R.layout.main); TextView tv = (TextView) findViewById(R.id.tv); new CountDownTimer(10000, 2000) { public void onTick(long m) { long sec = m/1000+1; tv.append(sec+" seconds remain\n"); } public void onFinish() { tv.append("Done!"); } }.start(); } 

Salida:
Quedan 10 segundos
Quedan 8 segundos
Quedan 6 segundos
Quedan 4 segundos
¡Hecho!

Problema:

¿Cómo logro que muestre ” quedan 2 segundos “? El tiempo transcurrido es de hecho 10 segundos, pero el último onTick () nunca ocurre. Si cambio el segundo parámetro de 2000 a 1000, este es el resultado:

Quedan 10 segundos
Quedan 9 segundos
Quedan 8 segundos
Quedan 7 segundos
Quedan 6 segundos
Quedan 5 segundos
Quedan 4 segundos
Quedan 3 segundos
Quedan 2 segundos
¡Hecho!

Como ve, parece saltarse la última llamada de Tick (). Y, por cierto, el archivo XML es básicamente el main.xml predeterminado con TextView asignado a id tv y el texto configurado a “”.

No sé por qué el último tic no está funcionando, pero puede crear su propio temporizador con Runable , por ejemplo.

 class MyCountDownTimer { private long millisInFuture; private long countDownInterval; public MyCountDownTimer(long pMillisInFuture, long pCountDownInterval) { this.millisInFuture = pMillisInFuture; this.countDownInterval = pCountDownInterval; } public void Start() { final Handler handler = new Handler(); Log.v("status", "starting"); final Runnable counter = new Runnable(){ public void run(){ if(millisInFuture <= 0) { Log.v("status", "done"); } else { long sec = millisInFuture/1000; Log.v("status", Long.toString(sec) + " seconds remain"); millisInFuture -= countDownInterval; handler.postDelayed(this, countDownInterval); } } }; handler.postDelayed(counter, countDownInterval); } } 

y para comenzar,

 new MyCountDownTimer(10000, 2000).Start(); 

EDITAR PARA LA PREGUNTA DE GOOFY

debe tener una variable para mantener el estado del contador (booleano). entonces puedes escribir un método Stop () como Start ().

EDIT-2 PARA LA PREGUNTA DE GOOFY

de hecho, no hay errores en detener el contador, pero hay un error al inicio de nuevo después de detener (reanudar).

Estoy escribiendo un nuevo código completo actualizado que acabo de probar y está funcionando. Es un contador básico que muestra el tiempo en la pantalla con el botón de inicio y parada.

clase para contador

 public class MyCountDownTimer { private long millisInFuture; private long countDownInterval; private boolean status; public MyCountDownTimer(long pMillisInFuture, long pCountDownInterval) { this.millisInFuture = pMillisInFuture; this.countDownInterval = pCountDownInterval; status = false; Initialize(); } public void Stop() { status = false; } public long getCurrentTime() { return millisInFuture; } public void Start() { status = true; } public void Initialize() { final Handler handler = new Handler(); Log.v("status", "starting"); final Runnable counter = new Runnable(){ public void run(){ long sec = millisInFuture/1000; if(status) { if(millisInFuture <= 0) { Log.v("status", "done"); } else { Log.v("status", Long.toString(sec) + " seconds remain"); millisInFuture -= countDownInterval; handler.postDelayed(this, countDownInterval); } } else { Log.v("status", Long.toString(sec) + " seconds remain and timer has stopped!"); handler.postDelayed(this, countDownInterval); } } }; handler.postDelayed(counter, countDownInterval); } } 

clase de actividad

 public class CounterActivity extends Activity { /** Called when the activity is first created. */ TextView timeText; Button startBut; Button stopBut; MyCountDownTimer mycounter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); timeText = (TextView) findViewById(R.id.time); startBut = (Button) findViewById(R.id.start); stopBut = (Button) findViewById(R.id.stop); mycounter = new MyCountDownTimer(20000, 1000); RefreshTimer(); } public void StartTimer(View v) { Log.v("startbutton", "saymaya basladi"); mycounter.Start(); } public void StopTimer(View v) { Log.v("stopbutton", "durdu"); mycounter.Stop(); } public void RefreshTimer() { final Handler handler = new Handler(); final Runnable counter = new Runnable(){ public void run(){ timeText.setText(Long.toString(mycounter.getCurrentTime())); handler.postDelayed(this, 100); } }; handler.postDelayed(counter, 100); } } 

main.xml

        

Revisé el código fuente de CountDownTimer. La “marca faltante” proviene de una característica especial de CountDownTimer que aún no he visto documentado en otra parte:

Al comienzo de cada tic, antes de llamar a Tick (), se calcula el tiempo restante hasta el final de la cuenta atrás. Si este tiempo es menor que el intervalo de tiempo de cuenta regresiva, ya no se invoca a Tick. En su lugar, solo se progtwig el siguiente tic (donde se llamará el método onFinish ()).

Dado el hecho de que los relojes de hardware no siempre son súper precisos, que puede haber otros procesos en segundo plano que retrasen el hilo que ejecuta CountDownTimer más que el propio Android probablemente creará un pequeño retraso al llamar al controlador de mensajes de CountDownTimer, es más que probable que la llamada para la última marca antes del final de la cuenta regresiva será al menos un milisegundo tarde y, por lo tanto, no se llamará a OnTick ().

Para mi aplicación, resolví este problema simplemente haciendo que los intervalos de marcación fueran “un poco” más pequeños (500 ms)

  myCountDownTimer = new CountDownTimer(countDownTime, intervalTime - 500) { ... } 

y podría dejar mi código tal como está. Para aplicaciones donde la duración del intervalo de tiempo es crítica, las otras soluciones publicadas aquí son probablemente las mejores.

La solución más simple que se me ocurrió es la siguiente. Tenga en cuenta que solo funciona si necesita una pantalla simple para mostrar con una cuenta regresiva de segundos.

 mTimer = new CountDownTimer(5000, 100){ public void onTick(long millisUntilFinished) { mTimerView.setText(Long.toString(millisUntilFinished/1000)); } public void onFinish() { mTimerView.setText("Expired"); } }; mTimer.start(); 

En el código anterior, se llama a onTick () cada 100 milisegundos, pero visualmente solo se muestran los segundos.

He pasado horas tratando de resolver este problema, y ​​estoy feliz de mostrarte un buen trabajo. No se moleste en esperar la llamada a onFinish() , simplemente agregue 1 (o cualquiera que sea su intervalo) a sus unidades, luego agregue una instrucción if en las llamadas onTick() . Simplemente haga su (s) tarea (s) de onTick() en el último onTick() . Esto es lo que tengo:

  new CountDownTimer( (countDownTimerValue + 1) * 1000, 1000) { //Added 1 to the countdownvalue before turning it into miliseconds by multiplying it by 1000. public void onTick(long millisUntilFinished) { //We know that the last onTick() happens at 2000ms remaining (skipping the last 1000ms tick for some reason, so just throw in this if statement. if (millisUntilFinished < 2005){ //Stuff to do when finished. }else{ mTextField.setText("Time remaining: " + (((millisUntilFinished) / 1000) - 1)); //My textfield is obviously showing the remaining time. Note how I've had to subtrack 1 in order to display the actual time remaining. } } public void onFinish() { //This is when the timer actually finishes (which would be about 1000ms later right? Either way, now you can just ignore this entirely. } }.start(); 

Si bien la solución anterior es válida, puede mejorarse aún más. Tiene innecesariamente un ejecutable dentro de otra clase (que ya puede tratarse por sí mismo). Así que solo crea una clase que amplíe un hilo (o ejecutable).

  class MyTimer extends Thread { private long millisInFuture; private long countDownInterval; final Handler mHandler = new Handler(); public MyTimer(long pMillisInFuture, long pCountDownInterval) { this.millisInFuture = pMillisInFuture; this.countDownInterval = pCountDownInterval; } public void run() { if(millisInFuture <= 0) { Log.v("status", "done"); } else { millisInFuture -= countDownInterval; mHandler.postDelayed(this, countDownInterval); } } } 

Encontré una solución fácil. Necesito que CountDown actualice ProgressBar, así que hice esto:

 new CountDownTimer(1000, 100) { private int counter = 0; @Override public void onTick(long millisUntilFinished) { Log.d(LOG_TAG, "Tick: " + millisUntilFinished); if (++counter == 10) { timeBar.setProgress(--lenght); // timeBar and lenght defined in calling code counter = 0; } } @Override public void onFinish() { Log.d(LOG_TAG, "Finish."); timeBar.setProgress(0); } }; 

Pequeña garrapata el truco 🙂

Así que creo que fui un poco más allá porque mi temporizador funciona en su propio hilo en lugar de utilizar los controladores postDelay, aunque siempre se vuelve a publicar en el hilo en el que se creó. También sabía que solo me importaban los segundos, así que se simplifica alrededor de eso. idea. También le permite cancelarlo y reiniciarlo. No tengo pausa incorporada porque eso no está en mis necesidades.

 /** * Created by MinceMan on 8/2/2014. */ public abstract class SecondCountDownTimer { private final int seconds; private TimerThread timer; private final Handler handler; /** * @param secondsToCountDown Total time in seconds you wish this timer to count down. */ public SecondCountDownTimer(int secondsToCountDown) { seconds = secondsToCountDown; handler = new Handler(); timer = new TimerThread(secondsToCountDown); } /** This will cancel your current timer and start a new one. * This call will override your timer duration only one time. **/ public SecondCountDownTimer start(int secondsToCountDown) { if (timer.getState() != State.NEW) { timer.interrupt(); timer = new TimerThread(secondsToCountDown); } timer.start(); return this; } /** This will cancel your current timer and start a new one. **/ public SecondCountDownTimer start() { return start(seconds); } public void cancel() { if (timer.isAlive()) timer.interrupt(); timer = new TimerThread(seconds); } public abstract void onTick(int secondsUntilFinished); private Runnable getOnTickRunnable(final int second) { return new Runnable() { @Override public void run() { onTick(second); } }; } public abstract void onFinish(); private Runnable getFinishedRunnable() { return new Runnable() { @Override public void run() { onFinish(); } }; } private class TimerThread extends Thread { private int count; private TimerThread(int count) { this.count = count; } @Override public void run() { try { while (count != 0) { handler.post(getOnTickRunnable(count--)); sleep(1000); } } catch (InterruptedException e) { } if (!isInterrupted()) { handler.post(getFinishedRunnable()); } } } 

}

Agregue algunos milisegundos a su temporizador para permitirle tiempo para procesar el código. Math.ceil() +100 a su longitud de temporizador, y también Math.ceil() para redondear el resultado, en lugar de agregar 1.

Además … el primer tic es DESPUÉS de 2000 milis, por lo que no obtendrá una entrada “10 segundos restantes” a menos que la agregue.

 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tv = (TextView) findViewById(R.id.tv); tv.setText("10 Seconds remain\n"); //displayed before the first tick. new CountDownTimer(10000+25, 1000) { //25 to account for processing time public void onTick(long m) { long sec = (long) Math.ceil(m / 2000 ); //round up, don't add 1 tv.append(sec + " seconds remain\n"); } public void onFinish() { tv.append("Done!"); } }.start(); } 

Para ampliar la respuesta de Nantoka. Aquí está mi código para asegurar que la vista se actualice correctamente:

 countDownTimer = new CountDownTimer(countDownMsec, 500) { public void onTick(long millisUntilFinished) { if(millisUntilFinished!=countDownMsec) { completedTick+=1; if(completedTick%2==0) // 1 second has passed { // UPDATE VIEW HERE based on "seconds = completedTick/2" } countDownMsec = millisUntilFinished; // store in case of pause } } public void onFinish() { countDownMsec = 0; completedTick+=2; // the final 2 ticks arrive together countDownTimer = null; // FINAL UPDATE TO VIEW HERE based on seconds = completedTick/2 == countDownMsec/1000 } } 

si su intervalo de tiempo es más de 4 segundos, entonces cada llamada onTick() no sería adecuada. Entonces, si quiere un resultado preciso, mantenga el intervalo inferior a 5 segundos. El Reseaon está al inicio de cada tic, antes de onTick() , se calcula el tiempo restante hasta el final de la cuenta regresiva y Si este tiempo es menor que el intervalo de tiempo de cuenta atrás, onTick() ya no se onTick() llamar. En su lugar, solo se progtwig el siguiente tic (donde se onFinish() método onFinish() ).

También me enfrenté al mismo problema con CountDownTimer y probé diferentes enfoques. Por lo tanto, una de las maneras más sencillas es la solución provista por @Nantoca; sugiere duplicar la frecuencia de 1000 ms a 500 ms. Pero no me gusta esta solución porque hace más trabajo que consumirá algún recurso de batería adicional.

Así que decidí usar el alma de @ ocanal y escribir mi propio CustomCountDownTimer simple.

Pero encontré algunas fallas en su código:

  1. Es un poco ineficiente (crear un segundo controlador para publicar resultados)

  2. Comienza a publicar el primer resultado con un retraso. ( postDelayed() realizar un método post() lugar de postDelayed() durante la primera inicialización)

  3. aspecto extraño. Métodos con mayúscula, estado en lugar de clásico isCanceled boolean y algunos otros.

Así que lo limpie un poco y aquí está la versión más común de su enfoque:

 private class CustomCountDownTimer { private Handler mHandler; private long millisUntilFinished; private long countDownInterval; private boolean isCanceled = false; public CustomCountDownTimer(long millisUntilFinished, long countDownInterval) { this.millisUntilFinished = millisUntilFinished; this.countDownInterval = countDownInterval; mHandler = new Handler(); } public synchronized void cancel() { isCanceled = true; mHandler.removeCallbacksAndMessages(null); } public long getRemainingTime() { return millisUntilFinished; } public void start() { final Runnable counter = new Runnable() { public void run() { if (isCanceled) { publishUpdate(0); } else { //time is out if(millisUntilFinished <= 0){ publishUpdate(0); return; } //update UI: publishUpdate(millisUntilFinished); millisUntilFinished -= countDownInterval; mHandler.postDelayed(this, countDownInterval); } } }; mHandler.post(counter); } } 

Está calculando que el tiempo permanece incorrectamente. La callback obtiene la cantidad de milisegundos hasta la finalización de la tarea.

 public void onTick(long m) { long sec = m/1000+1; tv.append(sec+" seconds remain\n"); } 

debiera ser

 public void onTick(long m) { long sec = m/1000; tv.append(sec+" seconds remain\n"); } 

Nunca utilicé esta clase, pero parece que no recibirás una callback en el instante en que comienza, por lo que parece que te falta una entrada. por ejemplo, 10000 ms, 1000 ms por tick, obtendría un total de 9 repeticiones de llamada de actualización, no 10 – 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, finalizar.