web-dev-qa-db-fra.com

Android: CountDownTimer ignore 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:
10 secondes restent
8 secondes restantes
Il reste 6 secondes
4 secondes restent
Terminé! 

Problème: 

Comment puis-je l'obtenir pour indiquer "2 secondes restantes"? Le temps écoulé est effectivement de 10 secondes, mais le dernier onTick () ne se produit jamais. Si je change le deuxième paramètre de 2000 à 1000, voici le résultat: 

10 secondes restent
9 secondes restantes
8 secondes restantes
7 secondes restantes
6 secondes restantes
5 secondes restantes
4 secondes restantes
3 secondes restantes
2 secondes restantes
Terminé! 

Donc, vous voyez, il semble ignorer ce dernier appel à onTick (). Et d'ailleurs, le fichier XML est fondamentalement le fichier principal.xml par défaut avec TextView auquel l'identifiant tv et le texte défini sur "".

43

Je ne sais pas pourquoi le dernier tick ne fonctionne pas, mais vous pouvez créer votre propre minuterie avec Runable , 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 commencer,

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

EDIT POUR LA QUESTION DE GOOFY

vous devriez avoir une variable pour conserver le statut de compteur (booléen). alors vous pouvez écrire une méthode Stop () telle que Start ().

EDIT-2 POUR LA QUESTION DE GOOFY

en fait, il n'y a pas de bogue sur le compteur d'arrêt, mais il y en a un sur le redémarrage après l'arrêt.

J'écris un nouveau code complet mis à jour que je viens d'essayer et ça fonctionne. C'est un compteur de base qui affiche l'heure sur l'écran avec les boutons start et stop.

classe pour le 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>
22
ocanal

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

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

Étant donné que les horloges matérielles ne sont pas toujours très précises, il peut y avoir d'autres processus en arrière-plan qui retardent le thread exécutant CountDownTimer et qu'Android lui-même créera probablement un léger 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 onTick () ne sera pas appelé.

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

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

et je pourrais laisser mon code tel quel. Pour les applications où la durée de l'intervalle est critique, les autres solutions présentées ici sont probablement les meilleures.

48
Nantoka

La solution la plus simple que j'ai trouvée est la suivante. Notez que cela ne fonctionne que si vous avez besoin d'un simple écran à afficher avec un compte à rebours de 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, onTick () est appelé toutes les 100 millisecondes, mais visuellement, seules les secondes sont affichées.

4
Zzokk

J'ai passé des heures à essayer de résoudre ce problème et je suis heureux de vous montrer un bon travail. N'attendez pas l'appel de onFinish(), ajoutez simplement 1 (ou votre intervalle) à vos unités, puis ajoutez une instruction if dans les appels de onTick(). Il suffit de faire votre (vos) tâche (s) onFinish() sur le dernier onTick(). Voici 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();
3
ajwest

Bien que la solution ci-dessus soit valable, elle peut encore être améliorée. Il a inutilement un runnable dans 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 exécutable).

    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);
        }
      }
    }
3
Mike Welsh

Je pense donc que je suis allé un peu au-dessus de la réalité parce que mon chronomètre tourne dans son propre fil au lieu d'utiliser des gestionnaires postDelay, bien qu'il soit toujours posté dans le fil dans lequel il a été créé. idée. Il vous permet également de l’annuler et de le redémarrer. Je n'ai pas de pause intégrée car ce n'est pas dans mes besoins.

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

}

2
MinceMan

Ajoutez quelques millisecondes à votre chronomètre pour lui laisser le temps de traiter le code . J'ai ajouté +100 à la longueur de votre chronomètre et Math.ceil() pour arrondir le résultat plutôt que d'ajouter 1.

De plus, le premier tick estAPR&EGRAVE;S2000 millis, de sorte que vous n'obtiendrez pas une entrée "10 secondes restantes" à moins que vous ne l'ajoutiez.

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

J'ai trouvé une solution facile. J'ai besoin de CountDown pour mettre à jour ProgressBar, alors j'ai fait ceci:

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

};

Petite tique faire le tour :)

2
ryabenko-pro

Pour développer la réponse de Nantoka. Voici mon code pour m'assurer que la vue est mise à jour correctement:

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
    }
}
1
Simon Huckett

si votre intervalle de temps est supérieur à 4 secondes, alors chaque appel à onTick() ne sera pas correct. Donc, si vous voulez un résultat précis, gardez un intervalle inférieur à 5 secondes. La Reseaon est au début de chaque tick, avant l'appel de onTick(), le temps restant jusqu'à la fin du compte à rebours est calculé et si ce temps est inférieur à l'intervalle du compte à rebours, onTick() ne serait plus appelé. Au lieu de cela, seul le prochain tick (où la méthode onFinish() sera appelée) est planifié.

1
Kundan

J'ai également rencontré le même problème avec CountDownTimer et j'ai essayé différentes approches . L'un des moyens les plus simples est donc de résoudre la solution fournie par @Nantoca - il suggère de doubler la fréquence de 1000 ms à 500 ms. Mais je n'aime pas cette solution car elle nécessite plus de travail, ce qui consomme des ressources de batterie supplémentaires.

J'ai donc décidé d'utiliser l'âme de @ ocanal et d'écrire mon propre CustomCountDownTimer.

Mais j'ai trouvé quelques défauts dans son code:

  1. C'est un peu inefficace (créer un deuxième gestionnaire pour publier les résultats) 

  2. Il commence à publier le premier résultat avec un retard. (Vous devez utiliser une méthode post() plutôt que postDelayed() lors de la première initialisation)

  3. étrange à la recherche. Méthodes avec lettre majuscule, statut au lieu de classique isCanceled boolean et quelques autres.

J'ai donc nettoyé un peu et voici la version la plus commune de son approche:

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);
    }
}
0
Kirill Karmazin