2012-01-13 12 views
37

Kod:Android: CountDownTimer przeskakuje ostatni naTick()!

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

wyjściowa:
10 sekund pozostanie
8 sekund pozostają
6 sekund pozostają
4 sekundy pozostają
Gotowe!

Problem:

Jak mogę to pokazać "2 sekundy pozostają"? Czas, który upłynął, wynosi w rzeczywistości 10 sekund, ale ostatnia metoda onTick() nigdy się nie zdarza. W przypadku zmiany drugiego parametru od 2000 do 1000, to jest wyjście:

10 sekund nadal
9 sekundy pozostaje
8 sekund pozostają
7 sekund pozostają
6 sekundy pozostaje
5 sekund pozostają
Pozostało 4 sekundy
Pozostało 3 sekundy
Pozostało 2 sekundy
Zrobione!

Wygląda na to, że omijamy ostatnie wywołanie onTick(). I przy okazji plik XML jest w zasadzie domyślnym plikiem main.xml z tekstem TextView przypisanym do identyfikatora tv i tekstem ustawionym na "".

Odpowiedz

21

Nie wiem, dlaczego ostatni znacznik nie działa, ale można na przykład utworzyć własny zegar z Runable.

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

i go uruchomić,

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

edycji dla GOOFY Zapytanie

trzeba mieć zmienną do utrzymywania stanu licznika (logiczna). następnie możesz napisać metodę Stop(), taką jak Start().

EDIT-2 dla GOOFY Zapytanie

faktycznie nie ma błędów na zatrzymanie licznika, ale nie jest to problem na początku ponownie po zatrzymaniu (CV).

Piszę nowy zaktualizowany pełny kod, który właśnie wypróbowałem i działa. Jest to podstawowy licznik pokazujący czas na ekranie za pomocą przycisku start i stop.

klasa dla licznika

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

klasa aktywność

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

Pomyślałem, że powinienem użyć Handler'a, żeby się tym zająć, ale bez wady, o której mówię w moim pytaniu, CountDownTimer jest taki prosty i doskonały! Mam na myśli tylko przykład twojego kodu i moje, możesz zobaczyć, jak prosty i kompaktowy CountDownTimer jest porównywany do Handler'a! Jeśli tylko to faktycznie działało dobrze chociaż lol. – ProgrammingMakesMeQQ

+3

absolutnie masz rację, chcę tylko pokazać, że możesz to zrobić za pomocą alternatywnych metod. – ocanal

+0

@ocanal dziękuję używam twojego kodu, ale jeśli chcę zatrzymać timer jak to zrobić ... proszę odpowiedz – Goofy

0

Jesteś calcula pozostały niepoprawnie. Callback pobiera liczbę milisekund do zakończenia zadania.

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

powinny być

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

nigdy nie używałem tej klasy siebie, ale wygląda na to, że nie dostanie wywołania zwrotnego moment jego uruchomieniu, dlatego wydaje się, jakbyś brakuje wejście. na przykład 10000 ms, 1000 ms na tick, otrzymasz 9 callbacków aktualizacji, a nie 10 - 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, zakończenie.

+2

Zrobiłem to +1, aby wyjście miało sens. Dane wyjściowe, które mam w pytaniu, odzwierciedlają rzeczywisty czas pozostały do ​​zakończenia odliczania, np. kiedy mówi "3 sekundy ... 2 sekundy ... Zrobione!" oznacza to, że zostało naprawdę 2 sekundy przed "Gotowe!" pojawiła się wiadomość. Więc jeśli wyjmę "+1" z "long sec = m/1000", wtedy tekst powie "2 sekundy ... 1 sekunda ... Zrobione!" Nawet jeśli mówi "1 sekunda", naprawdę jest 2 sekundy do zrobienia. Brzmi to nieco myląco, ale jeśli wypróbujesz mój krótki kod, zobaczysz, o czym mówię. – ProgrammingMakesMeQQ

3

Spędziłem wiele godzin próbując rozgryźć ten problem i cieszę się, że mogę Ci pokazać dobrą robotę. Nie przejmuj się oczekiwaniem na połączenie onFinish(), po prostu dodaj 1 (lub jakikolwiek interwał) do jednostek, a następnie dodaj instrukcję if w rozmowach onTick(). Po prostu wykonaj swoje zadania onFinish() na ostatnim onTick(). Oto, co mam:

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

Podobają Ci się rozwiązania, ale uważam, że przynajmniej dla Androida> = 6 problem polega na tym, że ostateczny onTick() nie jest wywoływany, ale onFinish() wciąż jest wywoływany w odpowiednim czasie. Ponieważ aktualizuję tylko ekran odliczania w moim onTick, po prostu używam twojego rozwiązania, aby pokazać ostateczną aktualizację, ale pozostaw animacje onFinish() w tym samym miejscu. – rockhammer

3

Chociaż powyższe rozwiązanie jest prawidłowe, może być jeszcze ulepszone. Niepotrzebnie działa on w innej klasie (która może być już leczona we własnym zakresie). Stwórz więc klasę, która rozszerza wątek (lub działa).

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

Najprostszym rozwiązaniem, które wymyśliłem, jest: Zauważ, że działa tylko wtedy, gdy potrzebujesz prostego ekranu do wyświetlenia z odliczaniem sekund.

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

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

     mTimer.start(); 

W powyższym kodzie onTick() jest wywoływany co 100 milisekund, ale wyświetlane są tylko sekundy.

42

Sprawdziłem kod źródłowy CountDownTimer. "Brakujący tyk" pochodzi ze specjalnej funkcji CountDownTimer, której jeszcze nie widziałem udokumentowanego gdzie indziej:

Na początku każdego tiknięcia, przed wywołaniem onTick(), pozostały czas do zakończenia odliczania obliczony. Jeśli ten czas jest mniejszy niż przedział czasu odliczania, onTick jest już oznaczony jako , a nie. Zamiast tego zaplanowany jest tylko następny tyk (gdzie zostanie wywołana metoda onFinish()).

Biorąc pod uwagę fakt, że zegary sprzętowe nie zawsze są super precyzyjne, mogą istnieć inne procesy w tle, które opóźniają wątek z uruchomionym CountDownTimer oraz że sam system Android prawdopodobnie spowoduje małe opóźnienie podczas wywoływania procedury obsługi wiadomości CountDownTimer. bardziej niż prawdopodobne, że wezwanie do ostatniego tiku przed końcem odliczania będzie opóźnione o co najmniej jedną milisekundę, dlatego onTick() nie zostanie wywołany.

dla mojej aplikacji I rozwiązać ten problem po prostu poprzez odstępy podziałki „nieco” mniejsze (500 ms)

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

i mogę zostawić mojego kodu, tak jak to jest. W przypadku aplikacji, w których długość przedziału czasowego jest krytyczna, inne zamieszczone tutaj rozwiązania są prawdopodobnie najlepsze.

+1

Tak, alternatywnie możesz powiedzieć "countDownTime + 900" lub coś w tym stylu . Z czasem 10000 i interwałem 1000 zobaczysz tylko 10 tyknięć, jeśli każdy tik udaje się strzelić precyzyjnie na czas. Dodanie 900ms w tym przykładzie nie dałoby wystarczającej ilości czasu na 1 dodatkowy znacznik, ale da mu wystarczająco dużo czasu na "ostatnie" zaznaczenie z pewnym marginesem, aby zegar nie był precyzyjnie zaplanowany. (np. gdy ostatni tik trafi w "10004" ms od początku, to byś go nie zauważył, ale gdyby dodać trochę dopełnienia, byłoby dobrze) – JamieB

+0

Zmiana tyknięcia z 1000ms na 500ms, także dla mnie. – rsa

+0

To działa dla mnie! Wielkie dzięki!! –

2

Znalazłem łatwe rozwiązanie. Muszę CountDown zaktualizować ProgressBar, więc zrobiłem to:

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

}; 

mały haczyk rade :)

+0

działa, dziękuję –

2

Więc myślę, że poszedł trochę za burtę, bo mój zegar działa we własnym wątku zamiast korzystania Operatory postDelay, choć zawsze wracają do wątku, w którym zostały stworzone. Wiedziałem też, że zależy mi tylko na sekundach, więc jego uproszczenie wokół tego pomysłu. Umożliwia także anulowanie i ponowne uruchomienie. Nie mam wbudowanego wstrzymania, ponieważ nie jest to w moich potrzebach.

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

}

1

Aby rozwinąć na odpowiedź Nantoka użytkownika. Oto mój kod, aby zapewnić widok jest poprawnie zaktualizowane:

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 
    } 
} 
0

I również w obliczu tego samego problemu z CountDownTimer i próbowałem różnych podejść. Jednym z najprostszych sposobów jest rozwiązanie dostarczone przez @Nantoca - sugeruje podwojenie częstotliwości z 1000ms do 500ms. Ale nie podoba mi się to rozwiązanie, ponieważ powoduje więcej pracy, która pochłonie dodatkowe zasoby baterii.

Postanowiłem więc użyć duszy @ ocanal i napisać własny prosty CustomCountDownTimer.

Ale znalazłem kilka błędów w jego kodzie:

  1. To trochę nieskuteczny (tworzenie drugiego obsługi do publikowania wyników)

  2. Zaczyna opublikować pierwszy wynik z opóźnieniem. (Należy wykonać metodę post() zamiast postDelayed() podczas pierwszej inicjalizacji). Metody z wielką literą, status zamiast klasycznego to BooleanKanał i inne.

więc czyściłem go trochę i tutaj jest bardziej powszechna wersja jego podejście:

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

jeśli Twój czas Interval jest więcej niż 4 sek następnie każdy onTick() wezwanie nie byłoby właściwe. Jeśli chcesz uzyskać dokładny wynik, zachowaj odstęp mniejszy niż 5 sekund. Reseaon znajduje się na początku każdego tiku, przed wywołaniem onTick(), pozostały czas do zakończenia odliczania i jeśli ten czas jest mniejszy niż przedział czasu odliczania, onTick() nie będzie już wywoływany. Zamiast tego zostanie zaplanowany tylko następny znacznik (w którym zostanie wywołana metoda onFinish()).

Powiązane problemy