greezmky
greezmky

Reputation: 13

Android: How to run a loop controlled by one button (start/pause)

I am working on a program in java (android) I would like it to start and continue asking questions, upon button click, until the user is prompted for input, once input is received the program should resume asking the questions. The program should pause when the button is clicked again.

I am new to OOP and self taught. I thought that a thread was going to be the most practical way to solve the issue. I cannot get the program to both loop and allow user input. When trying to use HandlerThread I loose the ability to input data in the EditText. Could someone help with getting this loop to run on start click and loop after input?

I have a functional program that works when the button is cycled:

Main layout begins with "START" button, on click the start button turns to "PAUSE" and a seperate repeat button is made visible (and functional).

The values are generated, the question is asked, and the user is prompted by popping up the soft keyboard and setting the cursor in the EditText field.

Once an answer is received and the "enter/done" keyboard button is clicked, the answer will be evaluated against the saved values. But I cannot get the program to loop, or if I get it to loop it skips input completely and continues to as questions with no time for input.

Please direct me on code cleanliness if needed, I want to learn what I am doing incorrectly.

MAIN.java

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    // Create appropriate objects for buttons, edit text, and text to speech
    TextToSpeech tts;
    EditText txt;
    Button sbtn, rbtn;

    // Array and int to store numbers
    int[] num;
    int added = 0;

    // Boolean to check if questions is running
    public boolean isRunning;

    // Variables for random number range. TODO(Put into switch statement and list to select 1, 10, or 100s)
    static int maxNum = 100;
    static int minNum = 10;

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

        // Initializing buttons and others
        txt = findViewById(R.id.ans);
        sbtn = findViewById(R.id.strButton);
        rbtn = findViewById(R.id.rptButton);
        rbtn.setVisibility(View.INVISIBLE);

        // Initialize text to speech engine
        tts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int status) {
                if (status != TextToSpeech.ERROR) {
                    tts.setLanguage(Locale.ENGLISH);
                }
            }
        });

        // Start button click listener
        sbtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (!isRunning) {
                    // Show repeat button, hide start and show pause
                    rbtn.setVisibility(View.VISIBLE);
                    sbtn.setText("Pause");

                    process();

                    isRunning = true;

                } else {
                    sbtn.setText("Start");
                    rbtn.setVisibility(View.GONE);

                    isRunning = false;
                }
            }
        });

        // Repeat button click listener
        rbtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                // Repeat recently generated numbers
                Utilities.speakNums(num[0], num[1], tts);
            }
        });
    }

    public void onPause() {
        if (tts != null) {
            tts.stop();
            tts.shutdown();
        }
        super.onPause();
    }

    // Get input and compare with stored values, announce if user answer is correct or incorrect
    public void submitAns() {
        txt.setOnKeyListener(new View.OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                // If the event is a key-down event on the "enter/done" button
                if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
                        (keyCode == KeyEvent.KEYCODE_ENTER)) {
                    // Check to make sure the text field is not empty
                    if (TextUtils.isEmpty(txt.getText().toString())) {
                        Toast.makeText(MainActivity.this, "Enter a Number!", Toast.LENGTH_SHORT).show();
                        return false;
                    }
                    int intValue = Integer.parseInt(txt.getText().toString());

                    if (added == intValue) {
                        Toast.makeText(MainActivity.this, "Correct", Toast.LENGTH_SHORT).show();
                        tts.speak("Correct", TextToSpeech.QUEUE_ADD, null, null);
                    } else {
                        Toast.makeText(MainActivity.this, added + " is the Correct Answer!", Toast.LENGTH_SHORT).show();
                        tts.speak("Incorrect", TextToSpeech.QUEUE_FLUSH, null, null);
                        Utilities.speakAns(added, tts);
                        tts.speak("is the Correct answer", TextToSpeech.QUEUE_ADD, null, null);
                    }
                    txt.setText("");
                    return true;
                }
                return false;
            }
        });
    }
            public void process() {
            num = Utilities.askQuestion(minNum, maxNum, tts);
            added = Utilities.add(num[0], num[1]);
            Utilities.focus(txt, getApplicationContext());
            submitAns();
            }
}

UTILITIES.java

import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;

import java.util.Random;

public class Utilities {

    // Function to generate random numbers in range
    public static int randomGen(int minNum, int maxNum) {
        final Random randNum = new Random();
        return randNum.nextInt(maxNum - minNum) + minNum;
    }
    public static int add(int num1, int num2) {
        return num1 + num2;
    }
    public static int sub(int num1, int num2) {
        return num1 - num2;
    }

    // Speak individual numbers with operator in between speech
    public static void speakNums(int r1, int r2, TextToSpeech tts) {
        String toSpeak = Integer.toString(r1);
        String nexToSpeak = Integer.toString(r2);

        tts.speak(toSpeak, TextToSpeech.QUEUE_ADD, null, null);
        tts.speak("Plus", TextToSpeech.QUEUE_ADD, null, null);
        tts.speak(nexToSpeak, TextToSpeech.QUEUE_ADD, null, null);
    }

    // Speak answer
    public static void speakAns(int a, TextToSpeech tts) {
        String sumSpeak = Integer.toString(a);
        tts.speak(sumSpeak, TextToSpeech.QUEUE_ADD, null, null);
    }

    // Request focus so that keyboard pops up as generate button is tapped
    public static void focus(EditText txt, Context context) {
        txt.requestFocus();
        InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput(txt, InputMethodManager.SHOW_IMPLICIT);
    }

    // Generate question, speak question, and return array of      random numbers for other operations
    public static int[] askQuestion(int minNum, int maxNum, TextToSpeech tts) {
       int r1 = randomGen(minNum, maxNum);
       int r2 = randomGen(minNum, maxNum);
       speakNums(r1, r2, tts);
       return new int[] { r1, r2};
    }
}

Upvotes: 0

Views: 118

Answers (1)

Tyler V
Tyler V

Reputation: 10920

If you want to have a repeating/infinite series of events that involve user interaction (like answering a question) you should set it up as a series of chained events rather than trying to use an actual code loop or handler thread.

An example of how this could be set up is:

  1. Generate a question to show (e.g. "What is 12 + 54?")
  2. When the user is done answering, call a "submit" method that checks their answer and either shows an error or generates a new question to show.
  3. Repeat the cycle above for as long as you want. No loops or handler threads are needed for this.

In terms of architecture, separating out as much of the question-generation and answer-processing logic into a ViewModel will help you tremendously, then the activity can just observe the relevant state in the view model (like what question to show).

Here is a simple example based on the description and example code you provided. There is a ViewModel that handles creating a question, checking the answer, and advancing to a new question, and an Activity that observes the relevant state from the ViewModel. Try it out in a blank app project to understand how it works.

ViewModel

public class MainViewModel extends ViewModel {

    // Error string to observe - to show an error message or toast
    private final MutableLiveData<String> error = new MutableLiveData<>("");
    LiveData<String> getError() {
        return error;
    }

    // Current question to show
    private final MutableLiveData<String> question = new MutableLiveData<>("");
    LiveData<String> getQuestion() {
        return question;
    }

    // Text to show on the start/pause button
    private final MutableLiveData<String> startPauseButton = new MutableLiveData<>("START");
    LiveData<String> getStartPauseButton() {
        return startPauseButton;
    }

    // private internal state, e.g. current question,
    // expected answer, play/pause state
    private int expected = 0;
    private String current_question = "";
    private boolean playing = false;
    private final Random random = new Random();
    private final int minNum = 10;
    private final int maxNum = 100;

    private int getNumber() {
        return random.nextInt(maxNum - minNum) + minNum;
    }

    // Process a user's answer, and either show an error
    // message or generate a new question to show
    void submitAnswer(String ans) {
        try {
            int a = Integer.parseInt(ans);
            if( a == expected ) {
                generateNewQuestion();
                question.postValue(current_question);
            }
            else {
                error.postValue("Incorrect answer, try again");
            }
        }
        catch (NumberFormatException e) {
            error.postValue("Not a number - enter a number");
        }
    }

    private void generateNewQuestion() {
        int a = getNumber();
        int b = getNumber();
        expected = a + b;
        current_question = "What is " + a + " + " + b + "?";
    }

    void clearError() {
        error.postValue("");
    }

    // Called when the user clicks the play/pause button
    void clickStartPause() {
        playing = !playing;
        if( playing ) {
            startPauseButton.postValue("PAUSE");
            question.postValue(current_question);
        }
        else {
            startPauseButton.postValue("START");
            question.postValue("");
        }
    }

    public MainViewModel() {
        generateNewQuestion();
    }
}

Activity

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

    TextView question = findViewById(R.id.question);
    EditText answer = findViewById(R.id.answer);
    Button start = findViewById(R.id.start);
    Button submit = findViewById(R.id.submit);

    question.setVisibility(View.GONE);
    answer.setVisibility(View.GONE);
    submit.setVisibility(View.GONE);

    MainViewModel model = new ViewModelProvider(this).get(MainViewModel.class);

    // Observe the current question, and if it is blank
    // hide the question/answer/submit views
    final Observer<String> questionObserver = questionTxt -> {
        if( questionTxt == null || questionTxt.isEmpty() ) {
            question.setVisibility(View.GONE);
            answer.setVisibility(View.GONE);
            submit.setVisibility(View.GONE);
        }
        else {
            question.setVisibility(View.VISIBLE);
            answer.setVisibility(View.VISIBLE);
            submit.setVisibility(View.VISIBLE);
            question.setText(questionTxt);
        }
    };

    // Observe the error state, if it is non-blank show
    // a toast then reset the state (so the toast only shows once)
    final Observer<String> errorObserver = errorText -> {
        if( errorText != null && !errorText.isEmpty() ) {
            Toast.makeText(this, errorText, Toast.LENGTH_LONG).show();
            model.clearError();
        }
    };

    model.getError().observe(this, errorObserver);
    model.getQuestion().observe(this, questionObserver);
    model.getStartPauseButton().observe(this, start::setText);

    submit.setOnClickListener(v -> {
        model.submitAnswer(answer.getText().toString());
        answer.setText("");
    });

    start.setOnClickListener(v -> model.clickStartPause());
}

XML

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/question"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <EditText
        android:id="@+id/answer"
        android:hint="Answer"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/question"
        android:layout_width="0dp"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/answer"/>

    <Button
        android:id="@+id/submit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Submit"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/answer"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Upvotes: 1

Related Questions