52 votes

Android : CountDownTimer saute le dernier onTick() !

Code :

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();  
   }

Sortie :
Il reste 10 secondes
Il reste 8 secondes
Il reste 6 secondes
Il reste 4 secondes
C'est fait !

Problème :

Comment faire pour qu'il s'affiche " Il reste 2 secondes " ? Le temps écoulé est bien de 10 secondes, mais le dernier onTick() n'arrive jamais. Si je change le deuxième paramètre de 2000 à 1000, voici la sortie :

Il reste 10 secondes
Il reste 9 secondes
Il reste 8 secondes
Il reste 7 secondes
Il reste 6 secondes
Il reste 5 secondes
Il reste 4 secondes
Il reste 3 secondes
Il reste 2 secondes
C'est fait !

Donc vous voyez, il semble sauter le dernier appel onTick(). Et d'ailleurs, le fichier XML est essentiellement le main.xml par défaut avec le TextView assigné à l'id TV et le texte est défini comme "".

3 votes

Ce bug est corrigé dans Oreo

0 votes

Au cas où quelqu'un s'intéresserait à la rétrocompatibilité, bien que le problème ait été résolu dans Android 8, il est présent dans Android 7 et je n'ai pas vu qu'il fonctionne de la manière dont le PO pense qu'il devrait fonctionner dans Android 5 ou 6.

57voto

Nantoka Points 358

J'ai vérifié le code source de CountDownTimer. Le "tick manquant" provient d'une fonctionnalité spéciale de CountDownTimer que je n'ai pas encore vue être documentée ailleurs :

Au début de chaque tick, avant l'appel de onTick(), le temps restant jusqu'à la fin du compte à rebours est calculé. Si ce temps est inférieur à l'intervalle de temps du compte à rebours, onTick est no plus appelé. Au lieu de cela, seul le prochain tick (où la méthode onFinish() sera appelée) est programmé.

Étant donné que les horloges matérielles ne sont pas toujours très précises, qu'il peut y avoir d'autres processus en arrière-plan qui retardent le fil d'exécution de CountDownTimer et qu'Android lui-même créera probablement un petit retard lors de l'appel du gestionnaire de messages de CountDownTimer, il est plus que probable que l'appel du dernier tick avant la fin du compte à rebours aura au moins une milliseconde de retard et donc que onTick() ne sera pas appelé.

Pour mon application, j'ai résolu ce problème en réduisant "légèrement" les intervalles de ticks (500 ms).

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

et je pourrais laisser mon code tel qu'il est. Pour les applications où la longueur de l'intervalle de temps est critique, les autres solutions proposées ici sont probablement les meilleures.

4 votes

Oui, vous pourriez aussi dire "countDownTime+900" ou quelque chose comme ça. Avec un temps de décompte de 10000 et un intervalle de 1000, vous ne verrez que 10 ticks si chaque tick se déclenche précisément à l'heure. Dans cet exemple, l'ajout de 900 ms ne donnera pas assez de temps pour un tic-tac supplémentaire, mais suffisamment de temps pour le "dernier" tic-tac, avec une certaine marge pour que l'horloge ne soit pas exactement à l'heure. (par exemple, lorsque le dernier tic-tac est déclenché à "10004" ms du début, vous le manquerez, mais si vous ajoutez un peu de marge, tout ira bien).

0 votes

Changer le tick de 1000ms à 500ms m'a aussi permis de résoudre ce problème.

2 votes

Veuillez ajouter la suggestion de @JamieB à votre réponse. De cette façon, même si votre application requiert un temps spécifique entre les tics, cela fonctionnera. J'ai remarqué que sur mon appareil il y a un délai de 6ms. Donc countDownTime+200 devrait résoudre le problème dans la plupart des cas et le rendre presque méconnaissable pour l'utilisateur.

23voto

ocanal Points 5576

Je ne sais pas pourquoi le dernier tick ne fonctionne pas mais vous pouvez créer votre propre timer avec Exécutable par exemple.

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);
    }
}

et pour le démarrer,

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

EDIT POUR LA QUESTION DE GOOFY

vous devriez avoir une variable pour contenir l'état du compteur (booléen) . alors vous pouvez écrire une méthode Stop() comme Start().

EDIT-2 POUR LA QUESTION DE GOOFY

En fait, il n'y a pas de bug sur l'arrêt du compteur mais il y a un bug sur le redémarrage après l'arrêt (reprise).

Je suis en train d'écrire un nouveau code complet mis à jour que je viens d'essayer et qui fonctionne. C'est un compteur basique qui affiche le temps sur l'écran avec un bouton de démarrage et d'arrêt.

classe pour compteur

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);
    }
}

classe d'activité

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:weightSum="1">
    <TextView android:textAppearance="?android:attr/textAppearanceLarge" 
              android:text="TextView" android:layout_height="wrap_content" 
              android:layout_width="wrap_content" 
              android:id="@+id/time">
    </TextView>
    <Button android:text="Start" 
            android:id="@+id/start" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:onClick="StartTimer">
    </Button>
    <Button android:text="Stop" 
            android:id="@+id/stop" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:onClick="StopTimer">
    </Button>
</LinearLayout>

1 votes

Je me suis dit que je devais simplement utiliser un Handler pour s'en occuper, mais à part le défaut dont je parle dans ma question, le CountDownTimer est tellement simple et parfait ! Je veux dire que juste à partir de votre exemple de code et du mien, vous pouvez voir à quel point CountDownTimer est simple et compact comparé à un Handler ! Si seulement il fonctionnait correctement, lol.

3 votes

Vous avez tout à fait raison, je veux juste montrer que vous pouvez le faire avec des méthodes alternatives.

0 votes

@ocanal merci j'utilise votre code mais si je veux arrêter le timer comment faire...merci de me répondre

4voto

Age Points 125

J'ai passé des heures à essayer de résoudre ce problème, et je suis heureux de vous montrer une solution de contournement. Ne vous embêtez pas à attendre le onFinish() il suffit d'ajouter 1 (ou tout autre intervalle) à vos unités, puis d'ajouter une instruction if dans l'instruction onTick() appels. Faites juste votre onFinish() tâche(s) sur le dernier onTick() . Voilà ce que j'ai :

    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();

0 votes

J'aime votre solution mais je trouve que, au moins pour Android >=6, le problème est que le onTick() final n'est pas appelé mais le onFinish() est toujours appelé au bon moment. Puisque je ne mets à jour que l'affichage du compte à rebours dans mon onTick, j'utilise votre solution pour montrer cette mise à jour finale tout en laissant les animations onFinish() au même endroit.

4voto

Zzokk Points 573

La solution la plus simple que j'ai trouvée est la suivante. Notez qu'elle ne fonctionne que si vous avez besoin d'un simple écran à afficher avec un compte à rebours des secondes.

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

             public void onFinish() {
                 mTimerView.setText("Expired");
             }
        };

        mTimer.start();

Dans le code ci-dessus, le onTick() est appelé toutes les 100 millisecondes mais visuellement, seules les secondes sont affichées.

3voto

Mike Welsh Points 11

Bien que la solution ci-dessus soit valable, elle peut être encore améliorée. Elle a inutilement un runnable à l'intérieur d'une autre classe (qui peut déjà être traitée seule). Il suffit donc de créer une classe qui étend un thread (ou un runnable).

    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);
        }
      }
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X