Search code examples
javaandroidandroid-studiohandlercountdowntimer

Handler is still running even though the CountDownTimer expires


I created an Handler to show different images of dice while my runnable is running. Usually, when i press the "Roll" button my timer counts down from 3 and application shows to value of dice. But sometimes, images continue to change even though the timer expires.

I think that my runnable is still running in the background. So, Hhw i can stop my handler completely when timer expires ?

package com.basarballioz.dicerollerdroid;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.Random;

public class OneDiceActivity extends AppCompatActivity  {

TextView diceStatus;
Handler handler;
Runnable runnable;
ImageView diceView;
Button rollButton;
CountDownTimer rollTimer = null;
int diceNumber;
TextView diceResult;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.one_dice_activity);


    //MAKE APPLICATION FULLSCREEN
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);


    diceStatus = findViewById(R.id.diceStatus);
    rollButton = findViewById(R.id.rollButton);
    diceView = findViewById(R.id.diceView);
    diceResult = findViewById(R.id.diceResult);
    diceStatus.setText("Press Roll!");


    //Enable this ONLY IF you want to use OnClickListener method instead of onclick button
    /*diceView.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            //rollDice();
        }
    });*/
}


public void rollDice(View view) {

    diceStatus.setText("Rolling...");
    rollTimer = new CountDownTimer(3000, 1000) {
        @Override
        public void onTick(final long millisUntilFinished) {

            rollButton.setEnabled(false);
            handler = new Handler();
            runnable = new Runnable() {

            @Override
            public void run() {
                Random ranNumber = new Random();
                diceNumber = ranNumber.nextInt(6) + 1;

                switch (diceNumber) {
                    case 1:
                        diceView.setImageResource(R.drawable.dice1);
                        diceResult.setText("1");
                        break;
                    case 2:
                        diceView.setImageResource(R.drawable.dice2);
                        diceResult.setText("2");
                        break;
                    case 3:
                        diceView.setImageResource(R.drawable.dice3);
                        diceResult.setText("3");
                        break;
                    case 4:
                        diceView.setImageResource(R.drawable.dice4);
                        diceResult.setText("4");
                        break;
                    case 5:
                        diceView.setImageResource(R.drawable.dice5);
                        diceResult.setText("5");
                        break;
                    case 6:
                        diceView.setImageResource(R.drawable.dice6);
                        diceResult.setText("6");
                        break;
                    default:
                        break;
                }
                handler.postDelayed(runnable, 100);
            }
        };
        handler.post(runnable);
    }

        @Override
        public void onFinish() {
            rollButton.setEnabled(true);
            showDiceNumber();
            handler.removeCallbacks(runnable);
        }

    }.start();
}

public void showDiceNumber() {
    diceStatus.setText("Your dice is: ");
}

@Override
public void onBackPressed() {
    finish();
}

   @Override
   protected void onDestroy() {
   super.onDestroy();
   }

}

Application's GitHub link: https://github.com/basarballioz/Dice-Rollerdroid


Solution

  • This line

    new CountDownTimer(3000, 1000) 
    

    creates a countdown timer which invokes onTick after 1 second elapsed, after 2 seconds elapsed and possibly after 3 seconds elapsed (not relevant but worth verifying).

    On each of the invocations of onTick (2 or 3 invocations) you create a new handler and assign it:

    handler = new Handler();
    

    and post it. Everytime each handler completes (2 or 3 handlers now), they reschedule for 100 milliseconds later:

    handler.postDelayed(runnable, 100);
    

    Note the runnable instance is recreated as well which is not necessary.

    When onFinish is invoked (after 2 or 3 ticks) - there are 2 or 3 handlers running and repeating every 100 milliseconds.

    Only the last handler or (2nd or 3rd) is canceled by the onFinish and the initial 1 or 2 handlers continue to run.

    Picture...

    enter image description here

    You definitely don't need more than 1 handler and runnable instance - in fact, you don't need a handler at all since CountDownTimer is essentially doing this already. Whether you use a CountDownTimer or just a plain Handler with a counter is up to you.

    If the desire is to have the dice face (value shown) change every 100 millis for a duration of 3 seconds after a 1 second delay, then change the onTick to only start a handler once (or eliminate the handler altogether - see suggested code). (Note currently implementation starts changing dice after an initial 1 second.)

    (Probably need to state in post what you really want to have happen.)

    The referenced library is incorrectly implemented.

    I'd suggest a simpler approach that eliminates the redundancy of the CountDownTimer code:

    public void rollDice(View view) {
    
        diceStatus.setText("Rolling...");
        // Note the random is only created once.
        final Random ranNumber = new Random();
        rollButton.setEnabled(false);
    
        // updates every 100 millis after an initial 100milli delay.
        rollTimer = new CountDownTimer(3000, 100) {
            @Override
            public void onTick(final long millisUntilFinished) {
    
                diceNumber = ranNumber.nextInt(6) + 1;
    
                // use same switch statement as original post - sets the dice face.
            }
    
            @Override
            public void onFinish() {
                rollButton.setEnabled(true);
                showDiceNumber();
            }
    
        }.start();
    }
    

    And if you really need an initial 1-second delay before rolling then don't immediately start() it but add something like:

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            rollTimer.start();
        }
    }, 1000);