************************************************************************
strings.xml
************************************************************************
Flag Quiz
Settings
Number of Choices
Display 2, 4, 6 or 8 guess buttons
Regions
Regions to include in the quiz
Guess the country
%1$d guesses, %2$.02f%% correct
Incorrect!
One region must be selected. Setting North America as the default region.
Quiz will restart with your new settings
Question %1$d of %2$d
Reset Quiz
Image of the current flag in the quiz
North_America
************************************************************************
arrays.xml
************************************************************************
- Africa
- Asia
- Europe
- North_America
- Oceania
- South_America
- Africa
- Asia
- Europe
- North America
- Oceania
- South America
- 2
- 4
- 6
- 8
************************************************************************
colors.xml
************************************************************************
#3F51B5
#303F9F
#448AFF
#00CC00
#FF0000
************************************************************************
button_text_color.xml
************************************************************************
************************************************************************
incorrect_shake.xml
************************************************************************
************************************************************************
preferences.xml
************************************************************************
************************************************************************
MainActivity.java - onCreate()
************************************************************************
// keys for reading data from SharedPreferences
public static final String CHOICES = "pref_numberOfChoices";
public static final String REGIONS = "pref_regionsToInclude";
private boolean phoneDevice = true; // used to force portrait mode
private boolean preferencesChanged = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// set default values in the app's SharedPreferences
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// register listener for SharedPreferences changes
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(preferencesChangeListener);
// determine screen size
int screenSize = getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK;
// if device is a tablet, set phoneDevice to false
if (screenSize == Configuration.SCREENLAYOUT_SIZE_LARGE
|| screenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
phoneDevice = false;
}
// if running on phone-sized device, allow only portrait orientation
if (phoneDevice) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
************************************************************************
MainActivity.java - onStart()
************************************************************************
@Override
protected void onStart() {
super.onStart();
if (preferencesChanged) {
// now that the default preferences have been set
// initialize MainActivityFragment and start the quiz
MainActivityFragment quizFragment = (MainActivityFragment)
getSupportFragmentManager().findFragmentById(R.id.quizFragment);
quizFragment.updateGuessRows(PreferenceManager.getDefaultSharedPreferences(this));
quizFragment.updateRegions(PreferenceManager.getDefaultSharedPreferences(this));
quizFragment.resetQuiz();
preferencesChanged = false;
}
}
************************************************************************
MainActivity.java - onCreateOptionsMenu()
************************************************************************
// show menu if app is running on a phone or a portrait-oriented tablet
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// get the device's current orientation
int orientation = getResources().getConfiguration().orientation;
// display the app's menu only in portrait orientation
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
// inflate the menu
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
} else {
return false;
}
}
************************************************************************
MainActivity.java - onOptionsItemSelected()
************************************************************************
// displays the SettingsActivity when running a phone
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent preferencesIntent = new Intent(this, SettingsActivity.class);
startActivity(preferencesIntent);
return super.onOptionsItemSelected(item);
}
************************************************************************
MainActivity.java - OnSharedPreferenceChangeListener
************************************************************************
// listener for changes on the app's SharedPreferences
private OnSharedPreferenceChangeListener preferencesChangeListener =
new OnSharedPreferenceChangeListener() {
// called when the user changes the map's preferences
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
preferencesChanged = true; // user changes app settings
MainActivityFragment quizFragment = (MainActivityFragment)
getSupportFragmentManager().findFragmentById(R.id.quizFragment);
if (key.equals(CHOICES)) { // # of choices to display changed
quizFragment.updateGuessRows(sharedPreferences);
quizFragment.resetQuiz();
} else if (key.equals(REGIONS)) { // regions to include changed
Set regions = sharedPreferences.getStringSet(REGIONS, null);
if (regions != null && regions.size() > 0) {
quizFragment.updateRegions(sharedPreferences);
quizFragment.resetQuiz();
} else {
// must select one region
SharedPreferences.Editor editor = sharedPreferences.edit();
regions.add(getString(R.string.default_region));
editor.putStringSet(REGIONS, regions);
editor.apply();
Toast.makeText(MainActivity.this,
R.string.default_region_message,
Toast.LENGTH_SHORT).show();
}
}
Toast.makeText(MainActivity.this,
R.string.restarting_quiz,
Toast.LENGTH_SHORT).show();
}
};
************************************************************************
MainActivityFragment.java - fields (w/o private)
************************************************************************
// String used when logging error messages
private static final String LOG_TAG = MainActivityFragment.class.getSimpleName();
private static final int FLAGS_IN_QUIZ = 10;
List fileNameList; // flag file names
List quizCountriesList; // countries in current quiz
Set regionsSet; // world regions in current quiz
String correctAnswer; // correct country for the correct flag
int totalGuesses; // number of guesses made
int correctAnswers; // number of correct guesses
int guessRows; // number of rows displaying guess buttons
SecureRandom random; // used to randomize the quiz
Handler handler; // used to delay loading next flag (android.os)
Animation shakeAnimation; // animation for incorrect guess
LinearLayout quizLinearLayout; // layout that contains the quiz
TextView questionNumberTextView; // shows current question #
ImageView flagImageView; // displays a flag
LinearLayout[] guessLinearLayouts; // rows of answer buttons
TextView answerTextView; // displays correct answer
************************************************************************
MainActivityFragment.java - fields
************************************************************************
private List fileNameList; // flag file names
private List quizCountriesList; // countries in current quiz
private Set regionsSet; // world regions in current quiz
private String correctAnswer; // correct country for the correct flag
private int totalGuesses; // number of guesses made
private int correctAnswers; // number of correct guesses
private int guessRows; // number of rows displaying guess buttons
private SecureRandom random; // used to randomize the quiz
private Handler handler; // used to delay loading next flag (android.os)
private Animation shakeAnimation; // animation for incorrect guess
private LinearLayout quizLinearLayout; // layout that contains the quiz
private TextView questionNumberTextView; // shows current question #
private ImageView flagImageView; // displays a flag
private LinearLayout[] guessLinearLayouts; // rows of answer buttons
private TextView answerTextView; // displays correct answer
************************************************************************
MainActivityFragment.java - onCreateView()
************************************************************************
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_main, container, false);
fileNameList = new ArrayList<>();
quizCountriesList = new ArrayList<>();
random = new SecureRandom();
handler = new Handler();
// load the shake animation that's used for incorrect answers
shakeAnimation = AnimationUtils.loadAnimation(getActivity(), R.anim.incorrect_shake);
shakeAnimation.setRepeatCount(3); // animation repeats 3 times
// get references to GUI components
quizLinearLayout = (LinearLayout) view.findViewById(R.id.quizLinearLayout);
questionNumberTextView = (TextView) view.findViewById(R.id.questionNumberTextView);
flagImageView = (ImageView) view.findViewById(R.id.flagImageView);
guessLinearLayouts = new LinearLayout[4];
guessLinearLayouts[0] = (LinearLayout) view.findViewById(R.id.row1LinearLayout);
guessLinearLayouts[1] = (LinearLayout) view.findViewById(R.id.row2LinearLayout);
guessLinearLayouts[2] = (LinearLayout) view.findViewById(R.id.row3LinearLayout);
guessLinearLayouts[3] = (LinearLayout) view.findViewById(R.id.row4LinearLayout);
answerTextView = (TextView) view.findViewById(R.id.answerTextView);
// configure listeners for the guess buttons
for (LinearLayout row : guessLinearLayouts) {
for (int column = 0; column < row.getChildCount(); column++) {
Button button = (Button) row.getChildAt(column);
button.setOnClickListener(guessButtonListener);
}
}
// set questionNumberTextView's text
questionNumberTextView.setText(getString(R.string.question, 1, FLAGS_IN_QUIZ));
return view;
}
************************************************************************
MainActivityFragment.java - updateGuessRows()
************************************************************************
// update guessRows based on value in sharedPreferences
public void updateGuessRows(SharedPreferences sharedPreferences) {
// get the number of guess buttons that should be diplayed
String choices = sharedPreferences.getString(MainActivity.CHOICES, null);
guessRows = Integer.parseInt(choices) / 2;
// hide all quests button LinearLayout
for (LinearLayout layout : guessLinearLayouts) {
layout.setVisibility(View.GONE);
}
// display appropriate guess button LinearLayout
for (int row = 0; row < guessRows; row++) {
guessLinearLayouts[row].setVisibility(View.VISIBLE);
}
}
************************************************************************
MainActivityFragment.java - updateRegions()
************************************************************************
// update world regions for quiz based on values in SharedPrefences
public void updateRegions(SharedPreferences sharedPreferences) {
regionsSet = sharedPreferences.getStringSet(MainActivity.REGIONS, null);
}
************************************************************************
MainActivityFragment.java - resetQuiz()
************************************************************************
// set up and start the next quiz
public void resetQuiz() {
// use AssetManager to get image file names for enabled regions
AssetManager assets = getActivity().getAssets();
fileNameList.clear(); // empty list of already used image file names
try {
// loop through each region
for (String region : regionsSet) {
// get a list of all flag image files in this region
String[] paths = assets.list(region);
for (String path : paths) {
fileNameList.add(path.replace(".png", ""));
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error loading image file names", e);
}
correctAnswers = 0; // reset the number of correct answers made
totalGuesses = 0; // reset the total number of guesses the user mode
quizCountriesList.clear(); // clear prior list of quiz countries
int flagCounter = 1;
int numberOfFlags = fileNameList.size();
// add FLAGS_IN_QUIZ random file names to the quizCountriesList
while (flagCounter <= FLAGS_IN_QUIZ) {
int randomIndex = random.nextInt(numberOfFlags);
// get the random file name
String filename = fileNameList.get(randomIndex);
// if the region is enabled and it hasn't already been chosen
if (!quizCountriesList.contains(filename)) {
quizCountriesList.add(filename);
flagCounter++;
}
}
loadNextFlag(); // start the quiz by loading the first flag
}
************************************************************************
MainActivityFragment.java - loadNextFlag()
************************************************************************
// after the user guesses a correct flag, load the next flag
private void loadNextFlag() {
// get file name of the next flag and remove it from the list
String nextImage = quizCountriesList.remove(0);
correctAnswer = nextImage; // update the correct answer
answerTextView.setText(""); // clear answerTextView
// display current question number
questionNumberTextView.setText(getString(
R.string.question, (correctAnswers + 1), FLAGS_IN_QUIZ));
// extract the region from the next image's name
String region = nextImage.substring(0, nextImage.indexOf('-'));
// use AssetManager to load next image from assets folder
AssetManager assets = getActivity().getAssets();
// get an InputStream to the asset representing the next flag
// and try to use the InputStream
try (InputStream stream = assets.open(region + "/" + nextImage + ".png")) {
// load the asset as a Drawable and display on the flagImageView
Drawable flag = Drawable.createFromStream(stream, nextImage);
flagImageView.setImageDrawable(flag);
animate(false); // animate the flag onto the screen
} catch (IOException e) {
Log.e(LOG_TAG, "Error loading " + nextImage, e);
}
Collections.shuffle(fileNameList); // shuffle file names
// put the correct answer at the end of fileNameList
int correct = fileNameList.indexOf(correctAnswer);
fileNameList.add(fileNameList.remove(correct));
// add 2, 4, 6 or 8 guess Buttons based on the value
for (int row = 0; row < guessRows; row++) {
// place Buttons in currentTableRow
for (int column = 0; column < guessLinearLayouts[row].getChildCount(); column++) {
// get reference to Button to configure
Button newGuessButton = (Button) guessLinearLayouts[row].getChildAt(column);
newGuessButton.setEnabled(true);
// get country name and set is as newGuessButton's text
String fileName = fileNameList.get((row * 2) + column);
newGuessButton.setText(getCountryName(fileName));
}
}
// randomly replace one Button with the correct answer
int row = random.nextInt(guessRows); // pick random row
int column = random.nextInt(2); // pick random column
LinearLayout randomRow = guessLinearLayouts[row]; // get the row
String countryName = getCountryName(correctAnswer);
((Button) randomRow.getChildAt(column)).setText(countryName);
}
************************************************************************
MainActivityFragment.java - getCountryName()
************************************************************************
private String getCountryName(String name) {
return name.substring(name.indexOf('-') + 1).replace('_', ' ');
}
************************************************************************
MainActivityFragment.java - animate()
************************************************************************
// animates the entire quizLinearLayout on or off screen
private void animate(boolean animateOut) {
// prevent animation into the UI for the first flag
if (correctAnswers == 0) {
return;
}
// calcualte center x and center y
int centerX = (quizLinearLayout.getLeft() + quizLinearLayout.getRight()) / 2;
int centerY = (quizLinearLayout.getTop() + quizLinearLayout.getBottom()) / 2;
// calculate animation radius
int radius = Math.max(quizLinearLayout.getWidth(), quizLinearLayout.getHeight());
Animator animator;
// if the quizLinearLayout should animate out rathe than in
if (animateOut) {
// create circular reveal animation
animator = ViewAnimationUtils.createCircularReveal(
quizLinearLayout, centerX, centerY, radius, 0);
animator.addListener(new AnimatorListenerAdapter() {
// called when the animation finishes
@Override
public void onAnimationEnd(Animator animation) {
loadNextFlag();
}
});
} else { // if the quizLinearLayout should animate in
animator = ViewAnimationUtils.createCircularReveal(
quizLinearLayout, centerX, centerY, 0, radius);
}
animator.setDuration(500); // set animation duration to 500ms
animator.start();
}
************************************************************************
MainActivityFragment.java - guessButtonListener()
************************************************************************
// called when a guess Button is touched
private View.OnClickListener guessButtonListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Button guessButton = (Button) v;
String guess = guessButton.getText().toString();
String answer = getCountryName(correctAnswer);
totalGuesses++; // increment number of guesses the user has made
if (guess.equals(answer)) { // if the guess is correct
correctAnswers++; // increment the number of correct answers
// display correct answer in green text
answerTextView.setText(answer + "!");
answerTextView.setTextColor(getResources()
.getColor(R.color.correct_answer, getContext().getTheme())
);
disableButtons(); // disable all guess Buttons
// if the user has correctly identified FLAGS_IN_QUIZ flags
if (correctAnswers == FLAGS_IN_QUIZ) {
// DialogFragment to display quiz stats and start new quiz
DialogFragment quizResults =
new DialogFragment() {
// create an AlertDialog and return it
@NonNull
@Override
public Dialog onCreateDialog(Bundle bundle) {
AlertDialog.Builder builder
= new AlertDialog.Builder(getActivity());
builder.setMessage(getString(R.string.results,
totalGuesses,
(1000 / (double) totalGuesses)));
// "Reset Quiz" Button
builder.setPositiveButton(R.string.reset_quiz,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
resetQuiz();
}
});
return builder.create();
}
};
// use FragmentManager to display the DialogFragment
quizResults.setCancelable(false);
quizResults.show(getFragmentManager(), "quiz results");
} else { // answer is correct but quiz is not over
// load the next flag after a 2-second delay
handler.postDelayed(
new Runnable() {
@Override
public void run() {
animate(true); // animate the flag off the screen
}
}, 2000); // 2000 milliseconds for 2-second delay
}
} else { // answer was incorrect
flagImageView.startAnimation(shakeAnimation); // play shake
// display "Incorrect!" in red
answerTextView.setText(R.string.incorrect_answer);
answerTextView.setTextColor(getResources().getColor(
R.color.incorrect_answer, getContext().getTheme()));
guessButton.setEnabled(false); // disable incorrect answer
}
}
};
************************************************************************
MainActivityFragment.java - disableButtons()
************************************************************************
// utility method that disables all answer buttons
private void disableButtons() {
for (int row = 0; row < guessRows; row++) {
LinearLayout guessRow = guessLinearLayouts[row];
for (int i = 0; i < guessRow.getChildCount(); i++) {
guessRow.getChildAt(i).setEnabled(false);
}
}
}
************************************************************************
SettingsActivity.java
************************************************************************
public class SettingsActivity extends AppCompatActivity {
// inflates the GUI, displays Toolbar and adds "up" button
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
************************************************************************
SettingsActivityFragment.java
************************************************************************
public class SettingsActivityFragment extends PreferenceFragment {
// creates preferences GUI from preferences.xml file in res/xml
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); // load from xml
}
}
************************************************************************
AndroidManifest.xml
************************************************************************
************************************************************************
************************************************************************