4.7 MainActivityFragment Class

Class MainActivityFragment (Figs. 4.324.42)—a subclass of the Android Support Library’s Fragment class (package android.support.v4.app)—builds the Flag Quiz’s GUI and implements the quiz’s logic.

4.7.1 package and import Statements

Figure 4.32 shows the MainActivityFragment package statement and import statements. Lines 5–36 import the various Java and Android classes and interfaces that the app uses. We’ve highlighted the key import statements, and we discuss the corresponding classes and interfaces in Section 4.3 and as they’re encountered in Sections 4.7.24.7.11.


 1   // MainActivityFragment.java
 2   // Contains the Flag Quiz logic
 3   package com.deitel.flagquiz;
 4
 5   import java.io.IOException;
 6   import java.io.InputStream;
 7   import java.security.SecureRandom;
 8   import java.util.ArrayList;  
 9   import java.util.Collections;
10   import java.util.List;       
11   import java.util.Set;        
12
13   import android.animation.Animator;               
14   import android.animation.AnimatorListenerAdapter;
15   import android.app.AlertDialog;                  
16   import android.app.Dialog;                       
17   import android.content.DialogInterface;          
18   import android.content.SharedPreferences;        
19   import android.content.res.AssetManager;         
20   import android.graphics.drawable.Drawable;       
21   import android.os.Bundle;
22   import android.support.v4.app.DialogFragment;    
23   import android.support.v4.app.Fragment;          
24   import android.os.Handler;                       
25   import android.util.Log;                         
26   import android.view.LayoutInflater;
27   import android.view.View;
28   import android.view.View.OnClickListener;        
29   import android.view.ViewAnimationUtils;          
30   import android.view.ViewGroup;
31   import android.view.animation.Animation;         
32   import android.view.animation.AnimationUtils;    
33   import android.widget.Button;
34   import android.widget.ImageView;
35   import android.widget.LinearLayout;
36   import android.widget.TextView;
37


Fig. 4.32 | MainActivityFragment package statement, import statements.

4.7.2 Fields

Figure 4.33 lists class MainActivityFragment’s static and instance variables. The constant TAG (line 40) is used when we log error messages using class Log (Fig. 4.38) to distinguish this Activity’s error messages from others that are being written to the device’s log. The constant FLAGS_IN_QUIZ (line 42) represents the number of flags in the quiz.


38   public class MainActivityFragment extends Fragment {
39      // String used when logging error messages
40      private static final String TAG = "FlagQuiz Activity";
41
42      private static final int FLAGS_IN_QUIZ = 10;
43
44      private List<String> fileNameList; // flag file names               
45      private List<String> quizCountriesList; // countries in current quiz
46      private Set<String> regionsSet; // world regions in current quiz    
47      private String correctAnswer; // correct country for the current flag
48      private int totalGuesses; // number of guesses made
49      private int correctAnswers; // number of correct guesses
50      private int guessRows; // number of rows displaying guess Buttons
51      private SecureRandom random; // used to randomize the quiz
52      private Handler handler; // used to delay loading next flag       
53      private Animation shakeAnimation; // animation for incorrect guess
54
55      private LinearLayout quizLinearLayout; // layout that contains the quiz
56      private TextView questionNumberTextView; // shows current question #
57      private ImageView flagImageView; // displays a flag
58      private LinearLayout[] guessLinearLayouts; // rows of answer Buttons
59      private TextView answerTextView; // displays correct answer
60


Fig. 4.33 | MainActivityFragment fields.


Image Good Programming Practice 4.1

For readability and modifiability, use String constants to represent filenames String literals (such as those used as the names of files or to log error messages) that do not need to be localized, and thus are not defined in strings.xml.


Variable fileNameList (line 44) holds the flag-image file names for the currently enabled geographic regions. Variable quizCountriesList (line 45) holds the flag file names for the countries used in the current quiz. Variable regionsSet (line 46) stores the geographic regions that are enabled.

Variable correctAnswer (line 47) holds the flag file name for the current flag’s correct answer. Variable totalGuesses (line 48) stores the total number of correct and incorrect guesses the player has made so far. Variable correctAnswers (line 49) is the number of correct guesses so far; this will eventually be equal to FLAGS_IN_QUIZ if the user completes the quiz. Variable guessRows (line 50) is the number of two-Button LinearLayouts displaying the flag answer choices—this is controlled by the app’s settings (Section 4.7.4).

Variable random (line 51) is the random-number generator used to randomly pick the flags to include in the quiz and which Button in the two-Button LinearLayouts represents the correct answer. When the user selects a correct answer and the quiz is not over, we use the Handler object handler (line 52) to load the next flag after a short delay.

The Animation shakeAnimation (line 53) holds the dynamically inflated shake animation that’s applied to the flag image when an incorrect guess is made. Lines 55–59 contain variables that we use to manipulate various GUI components programmatically.

4.7.3 Overridden Fragment Method onCreateView

MainActivityFragment’s onCreateView method (Fig. 4.34) inflates the GUI and initializes most of the MainActivityFragment’s instance variables—guessRows and regionsSet are initialized when the MainActivity calls MainActivityFragment’s updateGuessRows and updateRegions methods. After calling the superclass’s onCreateView method (line 65), we inflate the MainActivityFragment’s GUI (line 66–67) using the LayoutInflater that method onCreateView receives as an argument. The LayoutInflater’s inflate method receives three arguments:

• The layout resource ID indicating the layout to inflate.

• The ViewGroup (layout object) in which the Fragment will be displayed, which is received as onCreateView’s second argument.

• A boolean indicating whether or not the inflated GUI needs to be attached to the ViewGroup in the second argument. In a fragment’s onCreateView method, this should always be false—the system automatically attaches a fragment to the appropriate host Activity’s ViewGroup. Passing true here would cause an exception, because the fragment’s GUI is already attached.

Method inflate returns a reference to a View that contains the inflated GUI. We store that in local variable view so that it can be returned by onCreateView after the MainActivityFragment’s other instance variables are initialized. [Note: We removed the autogenerated, empty, no-argument constructor from this class (which appeared before method onCreateView in the class definition created by the IDE), as the compiler provides a default constructor for any class without constructors.]


61      // configures the MainActivityFragment when its View is created
62      @Override
63      public View onCreateView(LayoutInflater inflater, ViewGroup container,
64         Bundle savedInstanceState) {                                       
65         super.onCreateView(inflater, container, savedInstanceState);
66         View view =
67            inflater.inflate(R.layout.fragment_main, container, false);
68
69         fileNameList = new ArrayList<>();      // diamond operator
70         quizCountriesList = new ArrayList<>();
71         random = new SecureRandom();
72         handler = new Handler();
73
74         // load the shake animation that's used for incorrect answers 
75         shakeAnimation = AnimationUtils.loadAnimation(getActivity(),  
76            R.anim.incorrect_shake);                                   
77         shakeAnimation.setRepeatCount(3); // animation repeats 3 times
78
79         // get references to GUI components
80         quizLinearLayout =
81            (LinearLayout) view.findViewById(R.id.quizLinearLayout);
82         questionNumberTextView =
83            (TextView) view.findViewById(R.id.questionNumberTextView);
84         flagImageView = (ImageView) view.findViewById(R.id.flagImageView);
85         guessLinearLayouts = new LinearLayout[4];
86         guessLinearLayouts[0] =
87            (LinearLayout) view.findViewById(R.id.row1LinearLayout);
88         guessLinearLayouts[1] =
89            (LinearLayout) view.findViewById(R.id.row2LinearLayout);
90         guessLinearLayouts[2] =
91            (LinearLayout) view.findViewById(R.id.row3LinearLayout);
92         guessLinearLayouts[3] =
93            (LinearLayout) view.findViewById(R.id.row4LinearLayout);
94         answerTextView = (TextView) view.findViewById(R.id.answerTextView);
95
96         // configure listeners for the guess Buttons
97         for (LinearLayout row : guessLinearLayouts) {
98            for (int column = 0; column < row.getChildCount(); column++) {
99               Button button = (Button) row.getChildAt(column);
100              button.setOnClickListener(guessButtonListener);
101           }
102        }
103
104        // set questionNumberTextView's text
105        questionNumberTextView.setText(
106           getString(R.string.question, 1, FLAGS_IN_QUIZ));
107        return view; // return the fragment's view for display
108     }
109


Fig. 4.34 | MainActivityFragment overridden Fragment method onCreateView.

Lines 69–70 create ArrayList<String> objects that will store the flag-image file names for the currently enabled geographical regions and the names of the countries in the current quiz, respectively. Line 71 creates the SecureRandom object for randomizing the quiz’s flags and guess Buttons. Line 72 creates the Handler object handler, which we’ll use to delay by two seconds the appearance of the next flag after the user correctly guesses the current flag.

Lines 75–76 dynamically load the shake animation that will be applied to the flag when an incorrect guess is made. AnimationUtils static method loadAnimation loads the animation from the XML file represented by the constant R.anim.incorrect_shake. The first argument indicates the Context containing the resources that will be animated—inherited Fragment method getActivity returns the Activity that hosts this Fragment. Activity is an indirect subclass of Context. Line 77 specifies the number of times the animation should repeat with Animation method setRepeatCount.

Lines 80–94 get references to various GUI components that we’ll programmatically manipulate. Lines 97–102 get each guess Button from the four guessLinearLayouts and register guessButtonListener (Section 4.7.10) as the OnClickListener—we implement this interface to handle the event raised when the user touches any of the guess Buttons.

Lines 105–106 set the text in questionNumberTextView to the String returned by calling an overloaded version of Fragment’s inherited method getString. The first argument to format is the String resource R.string.question, which represents the format String

Question %1$d of %2$d

This String contains placeholders for two integer values (as described in Section 4.4.5). The remaining arguments are the values to insert in the format String. Line 107 returns the MainActivityFragment’s GUI.

4.7.4 Method updateGuessRows

Method updateGuessRows (Fig. 4.35) is called from the app’s MainActivity when the app is launched and each time the user changes the number of guess Buttons to display with each flag. Lines 113–114 use the method’s SharedPreferences argument to get the String for the key MainActivity.CHOICES—a constant containing the name of the preference in which the SettingsActivityFragment stores the number of guess Buttons to display. Line 115 converts the preference’s value to an int and divides it by 2 to determine the value for guessRows, which indicates how many of the guessLinearLayouts should be displayed—each with two guess Buttons. Next, lines 118–119 hide all of the guessLinearLayouts, so that lines 122–123 can show the appropriate guessLinearLayouts based on the value of guessRows. The constant View.GONE (line 119) indicates that Android should not consider the sizes of the specified components when laying out the rest of the components in the layout. There is also the constant View.INVISIBLE, which simply hides the component, and any space allocated to the component remains empty on the screen.


110      // update guessRows based on value in SharedPreferences
111      public void updateGuessRows(SharedPreferences sharedPreferences) {
112         // get the number of guess buttons that should be displayed
113         String choices =                                           
114            sharedPreferences.getString(MainActivity.CHOICES, null);
115         guessRows = Integer.parseInt(choices) / 2;
116
117         // hide all quess button LinearLayouts
118         for (LinearLayout layout : guessLinearLayouts)
119            layout.setVisibility(View.GONE);
120
121         // display appropriate guess button LinearLayouts
122         for (int row = 0; row < guessRows; row++)
123            guessLinearLayouts[row].setVisibility(View.VISIBLE);
124      }
125


Fig. 4.35 | MainActivityFragment method updateGuessRows.

4.7.5 Method updateRegions

Method updateRegions (Fig. 4.36) is called from the app’s MainActivity when the app is launched and each time the user changes the world regions that should be included in the quiz. Lines 128–129 use the method’s SharedPreferences argument to get the names of all of the enabled regions as a Set<String>. MainActivity.REGIONS is a constant containing the name of the preference in which the SettingsActivityFragment stores the enabled world regions.


126      // update world regions for quiz based on values in SharedPreferences
127      public void updateRegions(SharedPreferences sharedPreferences) {
128         regionsSet =                                                  
129            sharedPreferences.getStringSet(MainActivity.REGIONS, null);
130      }
131


Fig. 4.36 | MainActivityFragment method updateRegions.

4.7.6 Method resetQuiz

Method resetQuiz (Fig. 4.37) sets up and starts a quiz. Recall that the images for the game are stored in the app’s assets folder. To access this folder’s contents, the method gets the app’s AssetManager (line 135) by calling the parent Activity’s getAssets method. Next, line 136 clears the fileNameList to prepare to load image file names for only the enabled geographical regions. Lines 140–146 iterate through all the enabled world regions. For each, we use the AssetManager’s list method (line 142) to get an array of the flag-image file names, which we store in the String array paths. Lines 144–145 remove the .png extension from each file name and place the names in the fileNameList. AssetManager’s list method throws IOExceptions, which are checked exceptions (so you must catch or declare the exception). If an exception occurs because the app is unable to access the assets folder, lines 148–150 catch the exception and log it for debugging purposes with Android’s built-in logging mechanism. Log static method e is used to log error messages. You can see the complete list of Log methods at


132      // set up and start the next quiz
133      public void resetQuiz() {
134         // use AssetManager to get image file names for enabled regions
135         AssetManager assets = getActivity().getAssets();               
136         fileNameList.clear(); // empty list of image file names
137
138         try {
139            // loop through each region
140            for (String region : regionsSet) {
141               // get a list of all flag image files in this region
142               String[] paths = assets.list(region);               
143
144               for (String path : paths)
145                  fileNameList.add(path.replace(".png", ""));
146            }
147         }
148         catch (IOException exception) {
149            Log.e(TAG, "Error loading image file names", exception);
150         }
151
152         correctAnswers = 0; // reset the number of correct answers made
153         totalGuesses = 0; // reset the total number of guesses the user made
154         quizCountriesList.clear(); // clear prior list of quiz countries
155
156         int flagCounter = 1;
157         int numberOfFlags = fileNameList.size();
158
159         // add FLAGS_IN_QUIZ random file names to the quizCountriesList
160         while (flagCounter <= FLAGS_IN_QUIZ) {
161            int randomIndex = random.nextInt(numberOfFlags);
162
163            // get the random file name
164            String filename = fileNameList.get(randomIndex);
165
166            // if the region is enabled and it hasn't already been chosen
167            if (!quizCountriesList.contains(filename)) {
168               quizCountriesList.add(filename); // add the file to the list
169               ++flagCounter;
170            }
171         }
172
173         loadNextFlag(); // start the quiz by loading the first flag
174      }
175


Fig. 4.37 | MainActivityFragment method resetQuiz.

Next, lines 152–154 reset the counters for the number of correct guesses the user has made (correctAnswers) and the total number of guesses the user has made (totalGuesses) to 0 and clear the quizCountriesList.

Lines 160–171 add 10 (FLAGS_IN_QUIZ) randomly selected file names to the quizCountriesList. We get the total number of flags, then randomly generate the index in the range 0 to one less than the number of flags. We use this index to select one image file name from fileNameList. If the quizCountriesList does not already contain that file name, we add it to quizCountriesList and increment the flagCounter. We repeat this process until 10 (FLAGS_IN_QUIZ) unique file names have been selected. Then line 173 calls loadNextFlag (Fig. 4.38) to load the quiz’s first flag.

4.7.7 Method loadNextFlag

Method loadNextFlag (Fig. 4.38) loads and displays the next flag and the corresponding set of answer Buttons. The image file names in quizCountriesList have the format

regionName-countryName

without the .png extension. If a regionName or countryName contains multiple words, they’re separated by underscores (_).


176      // after the user guesses a correct flag, load the next flag
177      private void loadNextFlag() {
178         // get file name of the next flag and remove it from the list
179         String nextImage = quizCountriesList.remove(0);
180         correctAnswer = nextImage; // update the correct answer
181         answerTextView.setText(""); // clear answerTextView
182
183         // display current question number
184         questionNumberTextView.setText(getString(                   
185            R.string.question, (correctAnswers + 1), FLAGS_IN_QUIZ));
186
187         // extract the region from the next image's name
188         String region = nextImage.substring(0, nextImage.indexOf('-'));
189
190         // use AssetManager to load next image from assets folder
191         AssetManager assets = getActivity().getAssets();         
192
193         // get an InputStream to the asset representing the next flag
194         // and try to use the InputStream
195         try (InputStream stream =                           
196            assets.open(region + "/" + nextImage + ".png")) {
197            // load the asset as a Drawable and display on the flagImageView
198            Drawable flag = Drawable.createFromStream(stream, nextImage);   
199            flagImageView.setImageDrawable(flag);                           
200
201            animate(false); // animate the flag onto the screen
202         }
203         catch (IOException exception) {
204            Log.e(TAG, "Error loading " + nextImage, exception);
205         }
206
207         Collections.shuffle(fileNameList); // shuffle file names
208
209         // put the correct answer at the end of fileNameList
210         int correct = fileNameList.indexOf(correctAnswer);
211         fileNameList.add(fileNameList.remove(correct));
212
213         // add 2, 4, 6 or 8 guess Buttons based on the value of guessRows
214         for (int row = 0; row < guessRows; row++) {
215            // place Buttons in currentTableRow
216            for (int column = 0;
217                 column < guessLinearLayouts[row].getChildCount();
218                 column++) {
219               // get reference to Button to configure
220               Button newGuessButton =
221                  (Button) guessLinearLayouts[row].getChildAt(column);
222               newGuessButton.setEnabled(true);
223
224               // get country name and set it as newGuessButton's text
225               String filename = fileNameList.get((row * 2) + column);
226               newGuessButton.setText(getCountryName(filename));
227            }
228         }
229
230         // randomly replace one Button with the correct answer
231         int row = random.nextInt(guessRows); // pick random row
232         int column = random.nextInt(2); // pick random column
233         LinearLayout randomRow = guessLinearLayouts[row]; // get the row
234         String countryName = getCountryName(correctAnswer);
235         ((Button) randomRow.getChildAt(column)).setText(countryName);
236      }
237


Fig. 4.38 | MainActivityFragment method loadNextFlag.

Line 179 removes the first name from quizCountriesList and stores it in nextImage. We also save this in correctAnswer so it can be used later to determine whether the user made a correct guess. Next, we clear the answerTextView and display the current question number in the questionNumberTextView (lines 184–185) using the formatted String resource R.string.question.

Line 188 extracts from nextImage the region to be used as the assets subfolder name from which we’ll load the image. Next we get the AssetManager, then use it in the try- with-resources statement to open an InputStream (package java.io) to read bytes from the flag image’s file. We use that stream as an argument to class Drawable’s static method createFromStream, which creates a Drawable object (package android.graphics.drawable). The Drawable is set as flagImageView’s item to display by calling its setImageDrawable method. If an exception occurs, we log it for debugging purposes (line 204). Next, we call the animate method with false to animate the next flag and answer Buttons onto the screen (line 201).

Next, line 207 shuffles the fileNameList, and lines 210–211 locate the correctAnswer and move it to the end of the fileNameList—later we’ll insert this answer randomly into the one of the guess Buttons.

Lines 214–228 iterate through the Buttons in the guessLinearLayouts for the current number of guessRows. For each Button:

• lines 220–221 get a reference to the next Button

• line 222 enables the Button

• line 225 gets the flag file name from the fileNameList

• line 226 sets Button’s text with the country name that method getCountryName (Section 4.7.8) returns.

Lines 231–235 pick a random row (based on the current number of guessRows) and column, then set the text of the corresponding Button.

4.7.8 Method getCountryName

Method getCountryName (Fig. 4.39) parses the country name from the image file name. First, we get a substring starting from the dash (-) that separates the region from the country name. Then we call String method replace to replace the underscores (_) with spaces.


238      // parses the country flag file name and returns the country name
239      private String getCountryName(String name) {
240         return name.substring(name.indexOf('-') + 1).replace('_', ' ');
241      }
242


Fig. 4.39 | MainActivityFragment method getCountryName.

4.7.9 Method animate

Method animate (Fig. 4.40) executes the circular reveal animation on the entire layout (quizLinearLayout) of the quiz to transition between questions. Lines 246–247 return immediately for the first question to allow the first question to just appear rather than animate onto the screen. Lines 250–253 calculate the screen coordinates of the center of the quiz UI. Lines 256–257 calculate the maximum radius of the circle in the animation (the minimum radius is always 0). The animate method accepts one parameter, animateOut, and can be used in two ways. Line 262 uses animateOut to determine whether the animation will show or hide the quiz.


243      // animates the entire quizLinearLayout on or off screen
244      private void animate(boolean animateOut) {
245         // prevent animation into the the UI for the first flag
246         if (correctAnswers == 0)
247            return;
248
249         // calculate center x and center y
250         int centerX = (quizLinearLayout.getLeft() +
251            quizLinearLayout.getRight()) / 2;
252         int centerY = (quizLinearLayout.getTop() +
253            quizLinearLayout.getBottom()) / 2;
254
255         // calculate animation radius
256         int radius = Math.max(quizLinearLayout.getWidth(),
257            quizLinearLayout.getHeight());
258
259         Animator animator;
260
261         // if the quizLinearLayout should animate out rather than in
262         if (animateOut) {
263            // create circular reveal animation                
264            animator = ViewAnimationUtils.createCircularReveal(
265               quizLinearLayout, centerX, centerY, radius, 0); 
266            animator.addListener(                                 
267               new AnimatorListenerAdapter() {                    
268                  // called when the animation finishes           
269                  @Override                                       
270                  public void onAnimationEnd(Animator animation) {
271                     loadNextFlag();                              
272                  }                                               
273               }                                                  
274            );                                                    
275         }
276         else { // if the quizLinearLayout should animate in
277            animator = ViewAnimationUtils.createCircularReveal(
278               quizLinearLayout, centerX, centerY, 0, radius); 
279         }
280
281         animator.setDuration(500); // set animation duration to 500 ms
282         animator.start(); // start the animation                      
283      }
284


Fig. 4.40 | MainActivityFragment method animate.

If animate is called with the value true, the method will animate the quizLinearLayout off the screen (lines 264–274). Lines 264–265 create a circular-reveal Animator object by calling ViewAnimationUtils method createCircularReveal. This method takes five parameters:

• The first specifies the View on which to apply the animation (quizLinearLayout).

• The second and third provide the x- and y-coordinates of the animation circle’s center.

• The last two determine the starting and ending radius of the animation’s circle.

Because this animates the quizLinearLayout off screen, its starting radius is the calculated radius and its ending radius is 0. Lines 266–274 create and associate an AnimatorListenerAdapter with the Animator. The AnimatorListenerAdapter’s onAnimationEnd (lines 269–272) method is called when the animation finishes and loads the next flag (line 271).

If animate is called with the value false, the method will animate the quizLinearLayout onto the screen at the start of the next question. Lines 277–278 create the Animator by calling the createCircularReveal method, but this time, we specify 0 for the starting radius and the calculated radius for the ending radius. This causes the quizLinearLayout to animate onto the screen rather than off the screen.

Line 281 calls Animator’s setDuration method to specify a duration of 500 milliseconds for the animation. Finally, line 282 starts the animation.

4.7.10 Anonymous Inner Class That Implements OnClickListener

In Fig. 4.34, lines 97–102 registered guessButtonListener (Fig. 4.41) as the event-handling object for each guess Button. Instance variable guessButtonListener refers to an anonymous-inner-class object that implements interface OnClickListener to respond to Button events. The method receives the clicked Button as parameter v. We get the Button’s text (line 290) and the parsed country name (line 291), then increment totalGuesses. If the guess is correct (line 294), we increment correctAnswers. Next, we set the answerTextView’s text to the country name and change its color to the color represented by the constant R.color.correct_answer (green), and we call our utility method disableButtons (Section 4.7.11) to disable all the answer Buttons.


285      // called when a guess Button is touched
286      private OnClickListener guessButtonListener = new OnClickListener() {
287         @Override
288         public void onClick(View v) {
289            Button guessButton = ((Button) v);
290            String guess = guessButton.getText().toString();
291            String answer = getCountryName(correctAnswer);
292            ++totalGuesses; // increment number of guesses the user has made
293
294            if (guess.equals(answer)) { // if the guess is correct
295               ++correctAnswers; // increment the number of correct answers
296
297               // display correct answer in green text
298               answerTextView.setText(answer + "!");
299               answerTextView.setTextColor(
300                  getResources().getColor(R.color.correct_answer,
301                     getContext().getTheme()));
302
303               disableButtons(); // disable all guess Buttons
304
305               // if the user has correctly identified FLAGS_IN_QUIZ flags
306               if (correctAnswers == FLAGS_IN_QUIZ) {
307                  // DialogFragment to display quiz stats and start new quiz      
308                  DialogFragment quizResults =                                    
309                     new DialogFragment() {                                       
310                        // create an AlertDialog and return it                    
311                        @Override                                                 
312                        public Dialog onCreateDialog(Bundle bundle) {             
313                           AlertDialog.Builder builder =                          
314                              new AlertDialog.Builder(getActivity());             
315                           builder.setMessage(                                    
316                              getString(R.string.results,                         
317                                 totalGuesses,                                    
318                                 (1000 / (double) totalGuesses)));                
319                                                                                  
320                           // "Reset Quiz" Button                                 
321                           builder.setPositiveButton(R.string.reset_quiz,         
322                              new DialogInterface.OnClickListener() {             
323                                 public void onClick(DialogInterface dialog,      
324                                    int id) {                                     
325                                    resetQuiz();                                  
326                                 }                                                
327                              }                                                   
328                           );                                                     
329                                                                                  
330                           return builder.create(); // return the AlertDialog     
331                        }                                                         
332                      };                                                          
333
334                  // use FragmentManager to display the DialogFragment           
335                  quizResults.setCancelable(false);                              
336                  quizResults.show(getFragmentManager(), "quiz results");        
337               }
338               else { // answer is correct but quiz is not over
339                  // load the next flag after a 2-second delay                   
340                  handler.postDelayed(                                           
341                     new Runnable() {                                            
342                        @Override                                                
343                        public void run() {                                      
344                           animate(true); // animate the flag off the screen     
345                        }                                                        
346                     }, 2000); // 2000 milliseconds for 2-second delay           
347               }
348            }
349            else { // answer was incorrect
350               flagImageView.startAnimation(shakeAnimation); // play shake
351
352               // display "Incorrect!" in red
353               answerTextView.setText(R.string.incorrect_answer);
354               answerTextView.setTextColor(getResources().getColor(
355                  R.color.incorrect_answer, getContext().getTheme()));
356               guessButton.setEnabled(false); // disable incorrect answer
357            }
358         }
359      };
360


Fig. 4.41 | Anonymous inner class that implements OnClickListener.

If correctAnswers is FLAGS_IN_QUIZ (line 306), the quiz is over. Lines 308–332 create a new anonymous inner class that extends DialogFragment (package android.support.v4.app) and will be used to display the quiz results. The DialogFragment’s onCreateDialog method uses an AlertDialog.Builder (discussed momentarily) to configure and create an AlertDialog for showing the quiz results, then returns it. When the user touches this dialog’s Reset Quiz Button, method resetQuiz is called to start a new game (line 325). Line 335 indicates that the dialog is not cancelable—the user must interact with the dialog, because touching outside the dialog or touching the back button will not return the user to the quiz. To display the DialogFragment, line 336 calls its show method, passing as arguments the FragmentManager returned by getFragmentManager and a String. The second argument can be used with FragmentManager method getFragment-ByTag to get a reference to the DialogFragment at a later time—we don’t use this capability in this app.

If correctAnswers is less than FLAGS_IN_QUIZ, then lines 340–346 call the postDelayed method of Handler object handler. The first argument defines an anonymous inner class that implements the Runnable interface—this represents the task to perform, animate(true), which animates the flags and answer Buttons off the screen and starts the transition to the next question, some number of milliseconds into the future. The second argument is the delay in milliseconds (2000). If the guess is incorrect, line 350 invokes flagImageView’s startAnimation method to play the shakeAnimation that was loaded in method onCreateView. We also set the text on answerTextView to display "Incorrect!" in red (lines 353–355), then disable the guessButton that corresponds to the incorrect answer.


Image Look-and-Feel Observation 4.4

You can set an AlertDialog’s title (which appears above the dialog’s message) with AlertDialog.Builder method setTitle. According to the Android design guidelines for dialogs (http://developer.android.com/design/building-blocks/dialogs.html), most dialogs do not need titles. A dialog should display a title for “a high-risk operation involving potential loss of data, connectivity, extra charges, and so on.” Also, dialogs that display lists of options use the title to specify the dialog’s purpose.


Creating and Configuring the AlertDialog

Lines 313–329 use an AlertDialog.Builder to create and configure an AlertDialog. Lines 313–314 create the AlertDialog.Builder, passing the fragment’s Activity as the Context argument—the dialog will be displayed in the context of the Activity that hosts the MainActivityFragment. Next, lines 315–318 set the dialog’s message to a formatted String showing the quiz results—the resource R.string.results contains placeholders for the total number of guesses and the percentage of the total guesses that were correct.

In this AlertDialog, we need only one button that allows the user to acknowledge the message and reset the quiz. We specify this as the dialog’s positive Button (lines 321—328)—touching this Button indicates that the user acknowledges the message displayed in the dialog and dismisses the dialog. Method setPositiveButton receives the Button’s label (specified with the String resource R.string.reset_quiz) and a reference to the Button’s event handler. If the app does not need to respond to the event, you can specify null for the event handler. In this case, we provide an object of an anonymous inner class that implements interface DialogInterface.OnClickListener. You override this interface’s onClick method to respond to the event when the user touches the corresponding Button in the dialog.

4.7.11 Method disableButtons

Method disableButtons (Fig. 4.42) iterates through the guess Buttons and disables them. This method is called when the user makes a correct guess.


361      // utility method that disables all answer Buttons
362      private void disableButtons() {
363         for (int row = 0; row < guessRows; row++) {
364            LinearLayout guessRow = guessLinearLayouts[row];
365            for (int i = 0; i < guessRow.getChildCount(); i++)
366               guessRow.getChildAt(i).setEnabled(false);
367         }
368      }
369   }


Fig. 4.42 | MainActivityFragment method disableButtons.

4.8 SettingsActivity Class

Class SettingsActivity (Fig. 4.43) hosts the SettingsActivityFragment when the app is running in portrait orientation. Overridden method onCreate (lines 11–18) calls method setContentView to inflate the GUI defined by activity_settings.xml (represented by the resource R.layout.activity_settings), then displays the Toolbar defined in SettingsActivity’s layout. Line 17 displays on the app bar an up button that the user can touch to return to the parent MainActivity. The IDE added this line when you added the SettingsActivity to the project and specified its hierarchical parent (Section 4.4.12). We removed from the class the remaining autogenerated code that’s not used in this app. You can also remove the unused menu resource menu_settings.xml.


 1   // SettingsActivity.java
 2   // Activity to display SettingsActivityFragment on a phone
 3   package com.deitel.flagquiz;
 4
 5   import android.os.Bundle;
 6   import android.support.v7.app.AppCompatActivity;
 7   import android.support.v7.widget.Toolbar;
 8
 9   public class SettingsActivity extends AppCompatActivity {
10      // inflates the GUI, displays Toolbar and adds "up" button
11      @Override
12      protected void onCreate(Bundle savedInstanceState) {
13         super.onCreate(savedInstanceState);
14         setContentView(R.layout.activity_settings);
15         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
16         setSupportActionBar(toolbar);
17         getSupportActionBar().setDisplayHomeAsUpEnabled(true);
18      }
19   }


Fig. 4.43 | SettingsActivity displays the SettingsActivityFragment on a phone device and on a tablet device in portrait orientation.

4.9 SettingsActivityFragment Class

Class SettingsActivityFragment (Fig. 4.44) inherits from PreferenceFragment (package android.preference). When the SettingsActivityFragment is created, method onCreate (lines 10–14) builds the preferences GUI by calling inherited PreferenceFragment method addPreferencesFromResource to build the preferences GUI from the preferences.xml (Section 4.4.11). As the user interacts with the preferences GUI, the preferences are automatically stored into a SharedPreferences file on the device. If the file does not exist, it will be created; otherwise, it will be updated. We removed the other unused autogenerated code from this class.


 1   // SettingsActivityFragment.java
 2   // Subclass of PreferenceFragment for managing app settings
 3   package com.deitel.flagquiz;
 4
 5   import android.os.Bundle;
 6   import android.preference.PreferenceFragment;
 7
 8   public class SettingsActivityFragment extends PreferenceFragment {
 9      // creates preferences GUI from preferences.xml file in res/xml
10      @Override
11      public void onCreate(Bundle bundle) {
12         super.onCreate(bundle);
13         addPreferencesFromResource(R.xml.preferences); // load from XML
14      }
15   }


Fig. 4.44 | SettingsActivityFragment subclass of PreferenceFragment displays the app’s preferences.

4.10 AndroidManifest.xml

Figure 4.45 shows the Flag Quiz app’s autogenerated manifest. Each Activity in an app must be declared in AndroidManifest.xml; otherwise, Android will not know that the Activity exists and will not be able to launch it. When you created the app, the IDE declared MainActivity in AndroidManifest.xml (lines 11–21). The notation

.MainActivity

in line 12 indicates that the class is in the package specified in line 2 and is shorthand for

com.deitel.flagquiz.MainActivity

We added line 14, which we’ll discuss momentarily.


 1   <?xml version="1.0" encoding="utf-8"?>
 2   <manifest package="com.deitel.flagquiz"
 3      xmlns:android="http://schemas.android.com/apk/res/android">
 4
 5      <application
 6         android:allowBackup="true"
 7         android:icon="@mipmap/ic_launcher"
 8         android:label="@string/app_name"
 9         android:supportsRtl="true"
10         android:theme="@style/AppTheme">
11         <activity
12            android:name=".MainActivity"
13            android:label="@string/app_name"
14            android:launchMode="singleTop"
15            android:theme="@style/AppTheme.NoActionBar">
16            <intent-filter>
17               <action android:name="android.intent.action.MAIN"/>
18
19               <category android:name="android.intent.category.LAUNCHER"/>
20            </intent-filter>
21         </activity>
22         <activity                                              
23            android:name=".SettingsActivity"                    
24            android:label="@string/title_activity_settings"     
25            android:parentActivityName=".MainActivity"          
26            android:theme="@style/AppTheme.NoActionBar">        
27            <meta-data                                          
28               android:name="android.support.PARENT_ACTIVITY"   
29               android:value="com.deitel.flagquiz.MainActivity">
30         </activity>                                            
31      </application>
32
33   </manifest>


Fig. 4.45 | AndroidManifest.xml with SettingsActivity declared.

When you added the SettingsActivity to the project (Section 4.4.1), the IDE added SettingsActivity to the manifest file automatically (lines 22–30). If you were to create a new Activity without using the IDE’s tools, you’d have to declare the new Activity by inserting an <activity> element like the one in lines 22–30. For complete manifest file details, visit

Launch Mode

Line 14 specifies MainActivity’s launchMode. By default each Activity you create uses the "standard" launch mode. In this mode, when Android receives an Intent to launch the Activity, Android creates a new instance of that Activity.

Recall from Section 4.4.12 that you specified SettingsActivity’s hierarchical parent. Again, this enables Android to define on the app bar an up button that a user can touch to navigate back to the specified parent Activity. When the user touches this button and the parent Activity uses "standard" launch mode, Android uses an Intent to launch the parent Activity. This results in a new instance of MainActivity. This also causes the Flag Quiz app to crash in MainActivity’s OnSharedPreferenceChangeListener (Section 4.6.7) when it tries to update a quizFragment that no longer exists—it was defined in a different MainActivity instance.

Line 14 fixes this problem by changing MainActivity’s launchMode to "singleTop". With this launch mode, when the user touches the up button, Android brings the existing MainActivity to the foreground, rather than creating a new MainActivity object. For more information on the <activity> element’s lauchMode values, visit

4.11 Wrap-Up

In this chapter, you built a Flag Quiz app that tests a user’s ability to correctly identify country flags. A key feature of this chapter was using Fragments to create portions of an Activity’s GUI. You used two activities to display the MainActivityFragment and the SettingsActivityFragment when the app was running in portrait orientation. You used one Activity to display both Fragments when the app was running on a tablet in landscape orientation—thus, making better use of the available screen real estate. You used a subclass of PreferenceFragment to automatically maintain and persist the app’s settings and a subclass of DialogFragment to display an AlertDialog to the user. We discussed portions of a Fragment’s lifecycle and showed how to use the FragmentManager to obtain a reference to a Fragment so that you could interact with it programmatically.

In portrait orientation, you provided an icon for the MainActivity’s Settings menu item. This appeared on the app bar, so the user could touch it to display the SettingsActivity containing the SettingsActivityFragment. To launch the SettingsActivity, you used an explicit Intent. You saw how to obtain preferences from the app’s SharedPreferences file and how to edit that file using a SharedPreferences.Editor.

We showed how to use a Configuration object to determine whether the app was running on a tablet in landscape orientation. We demonstrated how to manage a large number of image resources using subfolders in the app’s assets folder and how to access those resources via an AssetManager. You created a Drawable from an image’s bytes by reading them from an InputStream, then displayed the Drawable in an ImageView.

You learned about additional subfolders of the app’s res folder—menu for storing menu resource files, anim for storing animation resource files and xml for storing XML data files. We discussed how to use qualifiers to create a folder for storing a layout that should be used only on large devices in landscape orientation. We also demonstrated how to use a color state list resource to ensure that the text in the Buttons is readable for both the enabled and disabled states.

You used Toasts to display minor error messages or informational messages that appear on the screen briefly. To display the next flag in the quiz after a short delay, you used a Handler, which executes a Runnable after a specified number of milliseconds. You learned that a Handler’s Runnable executes in the thread that created the Handler (the GUI thread in this app).

We defined an Animation in XML and applied it to the app’s ImageView when the user guessed incorrectly to provide visual feedback to the user. We also used ViewAnimationUtils to create a circular reveal Animator for transitioning between questions. You learned how to log exceptions for debugging purposes with Android’s built-in logging mechanism and class Log. You also used various classes and interfaces from the java.util package, including List, ArrayList, Collections and Set.

Finally, we presented the app’s AndroidManifest.xml file. We discussed the autogenerated <activity> elements. We also changed the MainActivity’s launchMode to "singleTop" so that the app used one instance of MainActivity, rather than creating a new one each time the user touches the up button on the app bar.

In Chapter 5, we present the Doodlz app, which uses Android’s graphics capabilities to turn a device’s screen into a virtual canvas. You’ll also learn about Android’s immersive mode and printing capabilities.

Self-Review Exercises

4.1 Fill in the blanks in each of the following statements:

a) To access the app’s assets folder’s contents, a method should get the app’s AssetManager by calling method _____________ (inherited indirectly from class ContextWrapper).

b) Files in the assets folders are accessed via a(n) _____________ (package android.content.res), which can provide a list of all of the file names in a specified subfolder of assets and can be used to access each asset.

c) A(n) _____________ animation moves a View within its parent.

d) By default, animations in an animation set are applied in parallel, but you can use the _____________ attribute to specify the number of milliseconds into the future at which an animation should begin. This can be used to sequence the animations in a set.

4.2 State whether each of the following is true or false. If false, explain why.

a) AnimationUtils static method loadAnimation loads an animation from an XML file that specifies the animation’s options.

b) Android does not provide a logging mechanism for debugging purposes.

c) ImageView’s adjustViewBounds property specifies whether or not the ImageView maintains the aspect ratio of its Drawable.

d) You load color and String array resources from the colors.xml and strings.xml files into memory by using the Activity’s Resources object.

e) Use activities to create reusable components and make better use of the screen real estate in a tablet app.

Answers to Self-Review Exercises

4.1

a) getAssets.

b) AssetManager.

c) translate.

d) android:startOffset.

4.2

a) True.

b) False. When exceptions occur, you can log them for debugging purposes with the built-in Log class’s methods.

c) True.

d) True.

e) False. Use Fragments to create reusable components and make better use of the screen real estate in a tablet app.

Exercises

4.3 Fill in the blanks in each of the following statements:

a) When the user selects an item from a Menu or touches a menu item displayed on the app bar, Activity method _____________ is called to respond to the selection.

b) To delay an action, we use a(n) _____________ (package android.os) object to execute a Runnable after a specified delay.

c) You can specify the number of times an animation should repeat with Animation method _____________ and perform the animation by calling View method startAnimation (with the Animation as an argument) on the ImageView.

d) A(n) _____________ is a collection of animations which make up a larger animation.

e) Android supports _____________ animations which allow you to animate any property of any object.

f) For the android:fromXDelta attribute, specifying the value -5%p indicates that the View should move to the by 5% of the parent’s width (p indicates “percent”).

g) A(n) _____________ briefly displays a message to the user.

h) A(n) _____________ resource file defines a color resource that changes colors based on a View’s state.

i) Android Studio’s _____________ is used to add material design icons to a project.

j) We implement interface _____________ to handle the events that occur when the user touches a button on an AlertDialog.

4.4 State whether each of the following is true or false. If false, explain why.

a) The base class of all fragments is BaseFragment (package android.app).

b) Like an Activity, each Fragment has a life cycle.

c) Fragments can be executed independently of a parent Activity.

Project Exercises

4.5 (Enhanced Flag Quiz App) Make the following enhancements to the Flag Quiz app:

a) Count the number of questions that were answered correctly on the first try. After all the questions have been answered, display a message describes how well the user performed on first guesses.

b) Keep track of the score as the user proceeds through the app. Give the user the most points for answering correctly on the first guess, fewer points for answering correctly on the next guess, etc.

c) Investigate class SharedPreferences, then store the top five high scores in the SharedPreferences file that also stores this app’s settings.

d) Add multiplayer functionality.

e) If the user guesses the correct flag, include a “bonus question” asking the user to name the capital of the country. If the user answers correctly on the first guess, add 10 bonus points to the score; otherwise, simply display the correct answer, then allow the user to proceed to the next flag.

4.6 (Road Sign Quiz App) Create an app that tests the user’s knowledge of road signs. Display a random sign image and ask the user to select the sign’s name. Visit http://mutcd.fhwa.dot.gov/ser-shs_millennium.htm for traffic sign images and information.

4.7 (U.S. State Quiz App) Using the techniques you learned in this chapter, create an app that displays an outline of a U.S. state and asks the user to identify the state. If the user guesses the correct state, include a “bonus question” asking the user to name the state’s capital. If the user answers correctly, add 10 bonus points to the score; otherwise, simply display the correct answer, then allow the user to proceed to the next state. Keep score as described in Exercise 4.5(c).

4.8 (Country Quiz App) Using the techniques you learned in this chapter, create an app that displays an outline of a country and asks the user to identify its name. If the user guesses the correct country, include a “bonus question” asking the user to name the country’s capital. If the user answers correctly, add 10 bonus points to the score; otherwise, simply display the correct answer, then allow the user to proceed to the next country. Keep score as described in Exercise 4.5(c).

4.9 (Android Programming Quiz App) Using the Android knowledge you’ve gained thus far, create a multiple-choice Android programming quiz using original questions that you create. Add multiplayer capabilities so you can compete against your classmates.

4.10 (Movie Trivia Quiz App) Create a movie trivia quiz app.

4.11 (Sports Trivia Quiz App) Create a sports trivia quiz app.

4.12 (Custom Quiz App) Create an app that allows the user to create a customized true/false or multiple-choice quiz. This is a great study aid. The user can input questions on any subject and include answers, then use it to study for a test or final exam.

4.13 (Lottery Number Picker App) Create an app that randomly picks lottery numbers. Ask the user how many numbers to pick and the maximum valid number in the lottery (set a maximum value of 99). Provide five possible lottery-number combinations to choose from. Include a feature that allows the user to easily pick from a list of five popular lottery games. Find five of the most popular lottery games in your area and research how many numbers must be picked for a lottery ticket and the highest valid number. Allow the user to tap the name of the lottery game to pick random numbers for that game.

4.14 (Craps Game App) Create an app that simulates playing the dice game of craps. In this game, a player rolls two dice. Each die has six faces—we’ve provided die images with the book’s examples. Each face contains one, two, three, four, five or six spots. After the dice have come to rest, the sum of the spots on the two top faces is calculated. If the sum is 7 or 11 on the first throw, the player wins. If the sum is 2, 3 or 12 on the first throw (called “craps”), the player loses (the “house” wins). If the sum is 4, 5, 6, 8, 9 or 10 on the first throw, that sum becomes the player’s “point.” To win, a player must continue rolling the dice until the point value is rolled. The player loses by rolling a 7 before rolling the point.

4.15 (Craps Game App Modification) Modify the craps app to allow wagering. Initialize the variable balance to 1000 dollars. Prompt the player to enter a wager. Check that wager is less than or equal to balance, and if it’s not, have the user reenter wager until a valid wager is entered. After a correct wager is entered, run one game of craps. If the player wins, increase balance by wager and display the new balance. If the player loses, decrease balance by wager, display the new balance, check whether balance has become zero and, if so, display the message "Sorry. You busted!"

4.16 (Computer-Assisted Instruction App) Create an app that will help an elementary school student learn multiplication. Select two positive one-digit integers. The app should then prompt the user with a question, such as

How much is 6 times 7?

The student then inputs the answer. Next, the app checks the student’s answer. If it’s correct, display one of the following messages:

Very good!
Excellent!
Nice work!
Keep up the good work!

and ask another question. If the answer is wrong, display one of the following messages:

No. Please try again.
Wrong. Try once more.
Don't give up!
No. Keep trying.

and let the student try the same question repeatedly until the student gets it right. Enhance the app to ask addition, subtraction and multiplication questions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.46.78