The problem is an OutOfMemoryException
:
java.lang.OutOfMemoryError at java.util.ArrayList.addAll(ArrayList.java:194) at
de.hackert.wwequiz2014.QuizScreen.CheckIfQuestionIsNew(QuizScreen.java:49) at
It is caused by adding items to an ArrayList
inside your CheckIfQuestionIsNew()
method. Most likely there is an infinite loop somewhere in there as you have to add A LOT of items - I'm talking thousands to millions - to a ListView
to get an OutOfMemoryException
. I gonna look through your code but I think you might identify the exact error faster as you are familiar with the code.
EDIT:
I think I found the error.
What exactly are you trying to do here:
ListDifficulty = QuizIDsPassedD1;
ListDifficulty.addAll(QuizIDsPassedD1);
Doesn't make much sense to me. This will duplicate all items in the list over and over again combined with the do/while loop this is most likely the culprit.
EDIT:
I commented your code from CheckIfQuestionIsNew()
:
boolean CheckIfQuestionIsNew(int element) {
List<Integer> ListDifficulty = new ArrayList<Integer>();
// What is this line suppsed to do? You are creating a new ArrayList, it is already empty why would you want to remove somthing here?
ListDifficulty.removeAll(ListDifficulty);
if (QuizDifficulty==1){
// Now you are adding items to the new list for reasons I don't understand
ListDifficulty.addAll(QuizIDsPassedD1);
}else if (QuizDifficulty==2){
// Same here
ListDifficulty.addAll(QuizIDsPassedD2);
}else if (QuizDifficulty==3){
// Same here
ListDifficulty.addAll(QuizIDsPassedD3);
}
if (ListDifficulty.contains(element))
return false;
// Where does this code belong? It is not part of the if statement above
// I added empty lines around it to make it more clear that this is a statement which is not contained in any if statement
ListDifficulty.add(element);
if (QuizDifficulty==1){
// What are you doing here? You remove all the items from the list and than add the List from above?
// This code does absolutely nothing and makes the ifs at the top and the ifs right here completely useless
QuizIDsPassedD1.removeAll((QuizIDsPassedD1));
QuizIDsPassedD1.addAll(ListDifficulty);
}else if (QuizDifficulty==2){
// Same here
QuizIDsPassedD2.removeAll((QuizIDsPassedD2));
QuizIDsPassedD2.addAll(ListDifficulty);
}else if (QuizDifficulty==3){
// Same here
QuizIDsPassedD3.removeAll((QuizIDsPassedD3));
QuizIDsPassedD3.addAll(ListDifficulty);
}
return true;
}
I guess my main confusion comes from this:
You call ShuffleQuiz();
in a do/while loop, but for what purpose? What are trying to achieve here? If you just want a new question or questions which has/have not been answered before why don't you implement ShuffleQuiz()
to return the new question directly instead of resorting to this - I guess - random picking and then check if it's ok in a loop?
EDIT:
Ok I have improved your code. Your old code was over 770 lines of code, my improved version is just below 210 lines of code. First I have created to two classes, Question
and Answer
. They respectively hold all the data associated with one question and with one answer. The Question
class looks like this:
public class Question {
private final int imageResId;
private final int questionTextResId;
private final List<Answer> answers = new ArrayList<Answer>();
public Question(int questionTextResId, int imageResId) {
this.imageResId = imageResId;
this.questionTextResId = questionTextResId;
}
public Question addAnswer(int answerTextResId, boolean correct) {
Answer answer = new Answer(answerTextResId, correct);
this.answers.add(answer);
return this;
}
public int getQuestionTextResId() {
return questionTextResId;
}
public int getImageResId() {
return imageResId;
}
public List<Answer> getAnswers() {
return answers;
}
}
As you can see nothing special. It has as member variables the resource id of the question text, the resource id of the image and a List
of Answers
. Additionally I have added an addAnswer()
method to conveniently add answers to a question.
The Answer
class looks like this:
public class Answer {
private final int answerTextResId;
private final boolean correct;
public Answer(int answerTextResId, boolean correct) {
this.answerTextResId = answerTextResId;
this.correct = correct;
}
public int getAnswerTextResId() {
return answerTextResId;
}
public boolean isCorrect() {
return correct;
}
}
As you can see nothing special again, but here are just two member variables, one is the resource id of the answer text and the other one is a boolean if this answer is correct or not.
Before showing you the complete code of my improved QuizScreen
Activity
I will explain to you all the changes I made and how it works. First I have created member variables for all the Views
you use. You should not call findViewById()
that often. When you save the reference in a member variable you never have to call findViewById()
again:
private Button buttonAntwort1;
private Button buttonAntwort2;
private Button buttonAntwort3;
private Button buttonAntwort4;
private TextView textViewFrage;
private ImageView imageViewBild;
private Button[] answerButtons;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz_screen);
this.imageViewBild = (ImageView) findViewById(R.id.imageViewBild);
this.textViewFrage = (TextView) findViewById(R.id.textViewFrage);
this.buttonAntwort1 = (Button) findViewById(R.id.buttonAntwort1);
this.buttonAntwort2 = (Button) findViewById(R.id.buttonAntwort2);
this.buttonAntwort3 = (Button) findViewById(R.id.buttonAntwort3);
this.buttonAntwort4 = (Button) findViewById(R.id.buttonAntwort4);
this.answerButtons = new Button[] { this.buttonAntwort1, this.buttonAntwort2, this.buttonAntwort3, this.buttonAntwort4 };
createQuestions();
startGame();
}
As you can see I have also created a Button[]
which contains all the Buttons
which can be used to answer questions. My solution is completely flexible in that regards. You can have Questions
with as many Answers
as you want and every Question
can have a different amount of Answers
. You just have to make sure that there are enough Buttons
in the Button[]
. If you want to have Question
with more than 4 answers simply add as many Buttons
as you need to this Button[]
and then just add the Questions
as explained further down. The rest is completely automatic and you don't have to worry about having too many Buttons
. All Buttons
that are not needed/used by a Question
are automatically hidden by making them invisible.
I have created 3 Lists
, each of them contains all the questions of one difficulty level, and one additional List
which will be a question queue. When the game is actually running we go through the question queue one after another. The question queue is generated once at the beginning of the game, so we don't have to pick questions at random all the time and check if they were answered.
private final List<Question> easyQuestions = new ArrayList<Question>();
private final List<Question> mediumQuesitons = new ArrayList<Question>();
private final List<Question> hardQuestions = new ArrayList<Question>();
private final List<Question> questionQueue = new ArrayList<Question>();
I also added 3 convenience methods to make it easy to fill those Lists
with Questions
:
private Question newEasy(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.easyQuestions.add(question);
return question;
}
private Question newMedium(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.mediumQuesitons.add(question);
return question;
}
private Question newHard(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.hardQuestions.add(question);
return question;
}
In the method createQuestions()
all the Questions
will be created using these 3 convenience methods, as I already said I didn't copy the Questions
you had in your source code, you are going to have to add them again here:
private void createQuestions() {
newEasy(R.string.question1_text, R.drawable.question1_picture1)
.addAnswer(R.string.question1_answer1, false).addAnswer(R.string.question1_answer2, true)
.addAnswer(R.string.question1_answer3, false).addAnswer(R.string.question1_answer4, false);
newMedium(R.string.question2_text, R.drawable.question2_picture1)
.addAnswer(R.string.question2_answer1, false).addAnswer(R.string.question2_answer2, true)
.addAnswer(R.string.question2_answer3, false).addAnswer(R.string.question2_answer4, false);
newHard(R.string.question3_text, R.drawable.question3_picture1)
.addAnswer(R.string.question3_answer1, false).addAnswer(R.string.question3_answer2, true)
.addAnswer(R.string.question3_answer3, false).addAnswer(R.string.question3_answer4, false);
}
As you can see you just call newEasy()
if you want to add an easy Question
, newMedium()
if you want to add a medium Question
or newHard()
if you want to add a hard Question
. You can simply daisy chain calls to addAnswer()
to add as many Answers
as you want to the Question
. This should all be pretty self explanatory.
After all the Questions
are created startGame()
will be called. You can restart the game at any point just by calling startGame()
so if you want to add the feature to restart the game you can do so quite simply. The startGame() method looks like this:
private void startGame() {
Collections.shuffle(this.easyQuestions);
Collections.shuffle(this.mediumQuesitons);
Collections.shuffle(this.hardQuestions);
this.questionQueue.clear();
this.questionQueue.addAll(this.easyQuestions);
this.questionQueue.addAll(this.mediumQuesitons);
this.questionQueue.addAll(this.hardQuestions);
this.questionIndex = 0;
moveToQuestion(0);
}
The Collections.shuffle()
at the top shuffles the Lists
, it reorders the elements randomly. In the middle we create our questionQueue
. First we clear()
it to remove all questions that were in there from a previous game, and then we add first the easy questions, then the medium ones and finally the hard ones. And we reset our questionIndex
to 0. The questionIndex
keeps track at which position we are in the quesitonQueue
. Finally at the bottom we call moveToQuestion(0);
to go to the first question in the queue.
The moveToQuestion()
method is again pretty straight forward but this time since this method is a little more complex I will add comments to it to explain it. the method looks like this:
private void moveToQuestion(int index) {
// First we check if we have reached the end of the queue
if(index < this.questionQueue.size()) {
// If not we get the current question
Question question = this.questionQueue.get(index);
// Here we set the question text to the TextView
int questionTextResId = question.getQuestionTextResId();
this.textViewFrage.setText(questionTextResId);
// And here the question image to the ImageView.
int imageResId = question.getImageResId();
this.imageViewBild.setImageResource(imageResId);
// We get the answers from the question and create two count variables for convenience
List<Answer> answers = question.getAnswers();
int answerCount = answers.size();
int buttonCount = this.answerButtons.length;
// We start a loop through all the answer buttons
for(int i = 0; i < buttonCount; i++) {
// We get the current button from the Button[] which contains all the answer buttons
Button button = this.answerButtons[i];
// There might not be as many answers as there are buttons, that's what we check here
if(i < answerCount) {
// If there is an answer for this button make it visible
button.setVisibility(View.VISIBLE);
// We get the answer and bind to the button by calling bindAnswerToButton()
Answer answer = answers.get(i);
bindAnswerToButton(button, answer);
} else {
// If no answer exists for the Button we make it invisible.
button.setVisibility(View.GONE);
}
}
} else {
// We have reached the end of the queue
// You have to decide what happens when the game is won
Toast toast = Toast.makeText(this, R.string.game_won, Toast.LENGTH_SHORT);
toast.show();
}
}
In bindAnswerToButton()
we set the text and the OnClickListener
of the Button
:
private void bindAnswerToButton(Button button, Answer answer) {
int answerTextResId = answer.getAnswerTextResId();
button.setText(answerTextResId);
button.setOnClickListener(new AnswerClickListener(answer));
}
As you can see the OnClickListener
is custom and takes an answer as parameter in its constructor. This custom OnClickListener
is what validates our answers and checks if we picked the right one. The custom OnClickListener
looks like this:
private class AnswerClickListener implements View.OnClickListener {
private final Answer answer;
private AnswerClickListener(Answer answer) {
this.answer = answer;
}
@Override
public void onClick(View v) {
if(this.answer.isCorrect()) {
gotoNextQuestion();
} else {
// You have to decide what happens when someone picks the wrong answer
Toast toast = Toast.makeText(QuizScreen.this, R.string.toast_wrong_answer, Toast.LENGTH_SHORT);
toast.show();
}
}
}
The only thing it really does is check in onClick
if the answer that has been passed to it is correct and if it is calls gotoNextQuestion()
to move to the next question. If the answer is not correct currently only a Toast
will be displayed. You have to decide what you want to happen in this case.
gotoNextQuestion()
is again just a convenience method, all it does is increment our questionIndex
and then calls moveToQuestion
with the incremented questionIndex
to move to the next Quesiton
:
private void gotoNextQuestion() {
this.questionIndex++;
moveToQuestion(this.questionIndex);
}
And that is pretty much it. That was the whole code. Remember, the only thing you have to do is add all the questions in createQuestions()
as I explained above. Here is the complete source code of the QuizScreen
Activity
:
public static class QuizScreen extends ActionBarActivity {
private final List<Question> easyQuestions = new ArrayList<Question>();
private final List<Question> mediumQuesitons = new ArrayList<Question>();
private final List<Question> hardQuestions = new ArrayList<Question>();
private final List<Question> questionQueue = new ArrayList<Question>();
private int questionIndex = 0;
private Button buttonAntwort1;
private Button buttonAntwort2;
private Button buttonAntwort3;
private Button buttonAntwort4;
private TextView textViewFrage;
private ImageView imageViewBild;
private Button[] answerButtons;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz_screen);
this.imageViewBild = (ImageView) findViewById(R.id.imageViewBild);
this.textViewFrage = (TextView) findViewById(R.id.textViewFrage);
this.buttonAntwort1 = (Button) findViewById(R.id.buttonAntwort1);
this.buttonAntwort2 = (Button) findViewById(R.id.buttonAntwort2);
this.buttonAntwort3 = (Button) findViewById(R.id.buttonAntwort3);
this.buttonAntwort4 = (Button) findViewById(R.id.buttonAntwort4);
this.answerButtons = new Button[] { this.buttonAntwort1, this.buttonAntwort2, this.buttonAntwort3, this.buttonAntwort4 };
createQuestions();
startGame();
}
private void createQuestions() {
newEasy(R.string.question1_text, R.drawable.question1_picture1)
.addAnswer(R.string.question1_answer1, false).addAnswer(R.string.question1_answer2, true)
.addAnswer(R.string.question1_answer3, false).addAnswer(R.string.question1_answer4, false);
newMedium(R.string.question2_text, R.drawable.question2_picture1)
.addAnswer(R.string.question2_answer1, false).addAnswer(R.string.question2_answer2, true)
.addAnswer(R.string.question2_answer3, false).addAnswer(R.string.question2_answer4, false);
newHard(R.string.question3_text, R.drawable.question3_picture1)
.addAnswer(R.string.question3_answer1, false).addAnswer(R.string.question3_answer2, true)
.addAnswer(R.string.question3_answer3, false).addAnswer(R.string.question3_answer4, false);
}
private Question newEasy(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.easyQuestions.add(question);
return question;
}
private Question newMedium(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.mediumQuesitons.add(question);
return question;
}
private Question newHard(int questionTextResId, int imageResId) {
Question question = new Question(questionTextResId, imageResId);
this.hardQuestions.add(question);
return question;
}
private void startGame() {
Collections.shuffle(this.easyQuestions);
Collections.shuffle(this.mediumQuesitons);
Collections.shuffle(this.hardQuestions);
this.questionQueue.clear();
this.questionQueue.addAll(this.easyQuestions);
this.questionQueue.addAll(this.mediumQuesitons);
this.questionQueue.addAll(this.hardQuestions);
this.questionIndex = 0;
moveToQuestion(0);
}
private void moveToQuestion(int index) {
if(index < this.questionQueue.size()) {
Question question = this.questionQueue.get(index);
int questionTextResId = question.getQuestionTextResId();
this.textViewFrage.setText(questionTextResId);
int imageResId = question.getImageResId();
this.imageViewBild.setImageResource(imageResId);
List<Answer> answers = question.getAnswers();
int answerCount = answers.size();
int buttonCount = this.answerButtons.length;
for(int i = 0; i < buttonCount; i++) {
Button button = this.answerButtons[i];
if(i < answerCount) {
button.setVisibility(View.VISIBLE);
Answer answer = answers.get(i);
bindAnswerToButton(button, answer);
} else {
button.setVisibility(View.GONE);
}
}
}
}
private void gotoNextQuestion() {
this.questionIndex++;
moveToQuestion(this.questionIndex);
}
private void bindAnswerToButton(Button button, Answer answer) {
int answerTextResId = answer.getAnswerTextResId();
button.setText(answerTextResId);
button.setOnClickListener(new AnswerClickListener(answer));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.quiz_screen, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
private class AnswerClickListener implements View.OnClickListener {
private final Answer answer;
private AnswerClickListener(Answer answer) {
this.answer = answer;
}
@Override
public void onClick(View v) {
if(this.answer.isCorrect()) {
gotoNextQuestion();
} else {
Toast toast = Toast.makeText(QuizScreen.this, R.string.toast_wrong_answer, Toast.LENGTH_SHORT);
toast.show();
}
}
}
}
If you have any further questions feel free to ask!