-->

sequential countdowntimer with handler wont update

2019-06-09 11:35发布

问题:

I was trying to build some sort of sequential countdown. Meaning, that I build up a queue of "exercises", each one containing a specific duration, which is the countdown time. In a custom Countdown class, I pop these exercises off the queue and use the duration as countdown. I want these countdowns to run one after another. For this I built a Countdown class, based on the code basis of the abstract class CountDownTimer.

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Locale;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.widget.Button;
import android.widget.TextView;

    public class ExerciseMeCountDownTimer {

    private static final int MSG_COUNTDOWN = 100;
    private static final int MSG_FINISH = 99;
    private ArrayDeque<Exercise> eq;
    private long mMillisInFuture;
    private int mCountdownInterval;
    private String name;
    private long mStopTimeInFuture;
    CountdownHandler cHandler;

    public ExerciseMeCountDownTimer(ArrayList<Exercise> elist,
            Button startStopButton, TextView countdownText,
            CountdownHandler cHandler) {

        this.cHandler = cHandler;
        eq = new ArrayDeque<Exercise>(elist);
        this.start();
    }

    public final void cancel() {
        mHandler.removeMessages(MSG);
    }

    private synchronized final ExerciseMeCountDownTimer start() {
        if (!eq.isEmpty()) {
            Exercise e = eq.pop();
            this.mMillisInFuture = Long.parseLong(e.getDuration());
            this.mCountdownInterval = 30;
            this.name = e.getName();
        } else {
            onFinish();
            return this;
        }

        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

    public void onTick(long millisUntilFinished) {
        Message msg = cHandler.obtainMessage(MSG_COUNTDOWN);
        Bundle data = new Bundle();
        String text = String.format(Locale.GERMANY, "%02d:%02d:%03d",
                millisUntilFinished / 100000, millisUntilFinished / 1000,
                millisUntilFinished % 1000);
        data.putString("countdown", text);
        msg.setData(data);
        cHandler.sendMessage(msg);

    }

    public void onFinish() {
        if (!eq.isEmpty()) {
            this.start();
        }

        Message msg = cHandler.obtainMessage(MSG_FINISH);
        Bundle data = new Bundle();
        String text = String.format(Locale.GERMANY, "00:00:000");
        data.putString("finish", text);
        msg.setData(data);
        cHandler.sendMessage(msg);
    }

    private static final int MSG = 1;

    // handles counting down
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            final long millisLeft = mStopTimeInFuture
                    - SystemClock.elapsedRealtime();

            if (millisLeft <= 0) {
                onFinish();
            } else if (millisLeft < mCountdownInterval) {
                // no tick, just delay until done
                sendMessageDelayed(obtainMessage(MSG), millisLeft);
            } else {
                long lastTickStart = SystemClock.elapsedRealtime();
                onTick(millisLeft);

                // take into account user's onTick taking time to
                // execute
                long delay = lastTickStart + mCountdownInterval
                        - SystemClock.elapsedRealtime();

                // special case: user's onTick took more than interval
                // to
                // complete, skip to next interval
                while (delay < 0)
                    delay += mCountdownInterval;

                sendMessageDelayed(obtainMessage(MSG), delay);
            }
        }

    };

    @Override
    public String toString() {
        return this.name;
    }
}

The important part is the sendMessage part, where I send the time left on the countdown to a handler of my MainActivity, which then, should update a textview.

import android.os.Handler;
import android.os.Message;

class CountdownHandler extends Handler {
private static final int MSG_COUNTDOWN = 100;
private static final int MSG_FINISH = 99;
private MainActivity mActivity;

CountdownHandler(MainActivity activity) {
    this.mActivity = activity;
}

@Override
public void handleMessage(Message msg) {
    if (msg.what == MSG_COUNTDOWN) {
        String text = msg.getData().getString("countdown");
        this.mActivity.sayLog(text);
    }
    if (msg.what == MSG_FINISH) {
        String text = msg.getData().getString("finish");
        this.mActivity.sayLog(text);
    }

}

And finally updates the textView in MainActivty

public void sayLog(String text) {
    countdown.setText(text);
}

ExerciseMeCountDownTimer is called by

new ExerciseMeCountDownTimer(elist, cHandler); 

on some onClick().

The problem is, that sometimes (actually most of the time) the textView is not updated properly. It stops updating on random times like 00:05:211 etc. Would anyone mind telling me why this is keeps happening? Maybe also adding a solution or at least some literature (maybe pointing out some sections) which I should read to understand the problem? I am also upen for alternative approaches, as I am new to this "handler", "threads" thing in android.

EDIT

  • the textview was updating, but the textview was clickable. Whenever I clicked on the textview it stopped updating!
  • as the accepted answer shows, I decided to use the direkt approach of updating the appropriate textview inside the onTick() method.

回答1:

Using Handler and things in this situation is making it overly complicated.

CountDownTimers onTick() and onFinish() both run on the UI Thread so updating TextViews and other Views from either method can be done easily just by passing a reference of the View to the constructor of the class, as you are already doing. Then you simply update it in the method needed.

// could create a member variable for the TextView with your other member variables
...
mTV;

then in your constructor assign it // removed reference to Handler--you already have reference to TextView here public ExerciseMeCountDownTimer(ArrayList elist, Button startStopButton, TextView countdownText) { mTV = countdownText;

then update in whichever method is needed

public void onTick(long millisUntilFinished) {

    String text = String.format(Locale.GERMANY, "%02d:%02d:%03d",
            millisUntilFinished / 100000, millisUntilFinished / 1000,
            millisUntilFinished % 1000);
    mTV.setText(text);  // set the text here


}

public void onFinish() {
    if (!eq.isEmpty()) {
        this.start();
    }