************************************************************************ 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 ************************************************************************ ************************************************************************ ************************************************************************