Android: CountDownTimer skips last onTick()!

前端 未结 12 631
眼角桃花
眼角桃花 2020-11-27 14:51

Code:

public class SMH extends Activity {  

    public void onCreate(Bundle b) {  
        super.onCreate(b);  
        setContentView(R.layou         


        
相关标签:
12条回答
  • 2020-11-27 15:15

    I also faced the same issue with CountDownTimer and I tried different approaches. So one of the easiest ways is in solution provided by @Nantoca - he suggests to double the frequency from 1000ms to 500ms. But I don't like this solution because it makes more work which will consume some extra battery resource.

    So I decided to use @ocanal's soultion and to write my own simple CustomCountDownTimer.

    But I found couple of flaws in his code:

    1. It's a bit inefficient (creating second handler to publish results)

    2. It starts to publish first result with a delay. (You need to do a post() method rather than postDelayed() during first initialization)

    3. odd looking. Methods with capital letter, status instead of classic isCanceled boolean and some other.

    So I cleaned it a bit and here is the more common version of his approach:

    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 讨论(0)
  • 2020-11-27 15:16

    I don't know why the last tick is not working but you can create your own timer with Runable , for example.

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

    and to start it,

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

    EDIT FOR GOOFY'S QUESTION

    you should have a variable to hold counter status (boolean) . then you can write a Stop() method like Start().

    EDIT-2 FOR GOOFY'S QUESTION

    actually there is no bug on stopping counter but there is a bug on start again after stop(resume).

    I'm writing a new updated full code that I had just tried and it's working. It's a basic counter that show time on screen with start and stop button.

    class for counter

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

    activity class

    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>
    
    0 讨论(0)
  • 2020-11-27 15:16

    So I think I went a little over board because my timer runs in its own thread instead of using postDelay handlers, though it always posts back to the thread it was created in. I also knew that I only cared about seconds so its simplified around that idea. It also lets you cancel it and restart it. I do not have pausing built in because that's not in my needs.

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

    }

    0 讨论(0)
  • 2020-11-27 15:18

    You are calculating time remaining incorrectly. The callback gets the number of milliseconds until completion of the task.

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

    should be

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

    I've never used this class myself but it looks like you will not get a callback the instant it starts, which is why it appears like you're missing an entry. e.g. 10000 ms, 1000 ms per tick you'd get a total of 9 update callbacks, not 10 - 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000, finish.

    0 讨论(0)
  • 2020-11-27 15:19

    Add a few milliseconds to your timer to allow it time to process the code. I added +100 to your timer-length, and also Math.ceil() to round up the result, rather than adding 1.

    Also... the first tick is AFTER 2000 millis, so you won't get a "10 seconds left" entry unless you add it.

    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();
    }
    
    0 讨论(0)
  • 2020-11-27 15:21

    The most simple solution I came up with is as follows. Note that it only works if you need a simple screen to display with a seconds countdown.

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

    In the code above the onTick() is called every 100 milliseconds but visually only seconds are displayed.

    0 讨论(0)
提交回复
热议问题