8. SpotOn Game App

Objectives

In this chapter you’ll:

• Create a simple gamepp that’s easy to code and fun to play.

• Goup animations that move and resize ImageViews with ViewPropertyAnimators.

• Respond to animation lifecycle events with an AnimatorListener.

• Process click events for ImageViews and touch events for the screen.

• Use the thread-safe ConcurrentLinkedQueue collection from the java.util.concurrent package to allow concurrent access to a collection from multiple threads.

• Use an Activity’s default SharedPreferences file.

Outline

8.1 Introduction

8.2 Test-Driving the SpotOn Game App

8.3 Technologies Overview

8.4 Building the App’s GUI and Resource Files

8.4.1 AndroidManifest.xml

8.4.2 main.xml RelativeLayout

8.4.3 untouched.xml ImageView for an Untouched Spot

8.4.4 life.xml ImageView for a Life

8.5 Building the App

8.5.1 SpotOn Subclass of Activity

8.5.2 SpotOnView Subclass of View

8.6 Wrap-Up

Self-Review Exercises | Answers to Self-Review Exercises | Exercises

8.1. Introduction

The SpotOn game tests a user’s reflexes by requiring the user to touch moving spots before they disappear (Fig. 8.1). The spots shrink as they move, making them harder to touch. The game begins on level one, and the user reaches each higher level by touching 10 spots. The higher the level, the faster the spots move—making the game increasingly challenging. When the user touches a spot, the app makes a popping sound and the spot disappears. Points are awarded for each touched spot (10 times the current level). Accuracy is important—any touch that isn’t on a spot decreases the score by 15 times the current level. The user begins the game with three additional lives, which are displayed in the bottom-left corner of the app. If a spot disappears before the user touches it, a flushing sound plays and the user loses a life. The user gains a life for each new level reached, up to a maximum of seven lives. When no additional lives remain and a spot’s animation ends without the spot being touched, the game ends (Fig. 8.2).

Image

Fig. 8.1. SpotOn game app.

Image

Fig. 8.2. Game Over alert showing final score and Reset Game button.

8.2. Test-Driving the SpotOn Game App

Opening and Running the App

Open Eclipse and import the SpotOn app project. Perform the following steps:

1. Open the Import dialog. Select File > Import... to open the Import dialog.

2. Import the SpotOn app project. In the Import dialog, expand the General node and select Existing Projects into Workspace, then click Next > to proceed to the Import Projects step. Ensure that Select root directory is selected, then click the Browse... button. In the Browse for Folder dialog, locate the SpotOn folder in the book’s examples folder, select it and click OK. Click Finish to import the project into Eclipse. The project now appears in the Package Explorer window at the left side of the Eclipse window.

3. Launch the SpotOn app. In Eclipse, right click the SpotOn project in the Package Explorer window, then select Run As > Android Application from the menu that appears.

Playing the Game

As spots appear on the screen, tap them with your finger (or the mouse in an AVD). Try not to allow any spot to complete its animation, as you’ll lose one of your remaining lives. The game ends when you have no lives remaining and a spot completes its animation without you touching it. [Note: This is an Android 3.1 app. At the time of this writing, AVDs for Android 3.0 and higher are extremely slow. If possible, you should run this app on an Android 3.1 device.]

8.3. Technologies Overview

Android 3.x and Property Animation

This is our first app that uses features of Android 3.0+. In particular, we use property animation—which was added to Android in version 3.0—to move and scale ImageViews. Android versions prior to 3.0 have two primary animation mechanisms:

Tweened View animations allow you to change limited aspects of a View’s appearance, such as where it’s displayed, its rotation and its size.

Frame View animations display a sequence of images.

For any other animation requirements, you have to create your own animations, as we did in Chapter 7. Unfortunately, View animations affect only how a View is drawn on the screen. So, if you animate a Button from one location to another, the user can initiate the Button’s click event only by touching the Button’s original screen location.

With property animation (package android.animation), you can animate any property of any object—the mechanism is not limited to Views. Moving a Button with property animation not only draws the Button in a different location on the screen, it also ensures that the user can continue to interact with that Button in its current location.

Property animations animate values over time. To create an animation you specify:

• the target object containing the property or properties to animate

• the property or properties to animate

• the animation’s duration

• the values to animate between for each property

• how to change the property values over time—known as an interpolator

The property animation classes are ValueAnimator and ObjectAnimator. ValueAnimator calculates property values over time, but you must specify an AnimatorUpdateListener in which you programmatically modify the target object’s property values. This can be useful if the target object does not have standard set methods for changing property values. ValueAnimator subclass ObjectAnimator uses the target object’s set methods to modify the object’s animated properties as their values change over time.

Android 3.1 added the new utility class ViewPropertyAnimator to simplify property animation for Views and to allow multiple properties to be animated in parallel. Each View now contains an animate method that returns a ViewPropertyAnimator on which you can chain method calls to configure the animation. When the last method call in the chain completes execution, the animation starts. We’ll use this technique to animate the spots in the game. For more information on animation in Android, see the following blog posts:

android-developers.blogspot.com/2011/02/animation-in-honeycomb.html
android-developers.blogspot.com/2011/05/
   introducing-viewpropertyanimator.html

Listening for Animation Lifecycle Events

You can listen for property-animation lifecycle events by implementing the interface AnimatorListener, which defines methods that are called when an animation starts, ends, repeats or is canceled. If your app does not require all four, you can extend class AnimatorListenerAdapter and override only the listener method(s) you need.

Touch Handling

Chapter 7 introduced touch handling by overriding Activity method onTouchEvent. There are two types of touches in the SpotOn game—touching a spot and touching elsewhere on the screen. We’ll register OnClickListeners for each spot (i.e., ImageView) to process a touched spot, and we’ll use onTouchEvent to process all other screen touches.

ConcurrentLinkedQueue and Queue

We use the ConcurrentLinkedQueue class (from package java.util.concurrent) and the Queue interface to maintain thread-safe lists of objects that can be accessed from multiple threads of execution in parallel.

8.4. Building the App’s GUI and Resource Files

In this section, you’ll build the GUI and resource files for the SpotOn game app. To save space, we do not show this app’s strings.xml resource file. You can view the contents of this file by opening it from the project in Eclipse.

8.4.1. AndroidManifest.xml

Figure 8.3 shows this app’s AndroidManifest.xml file. We set the uses-sdk element’s android:minSdkVersion attribute to "12" (line 5), which represents the Android 3.1 SDK. This app will run only on Android 3.1+ devices and AVDs. Line 7 sets the attribute android:hardwareAccelerated to "true". This allows the app to use hardware accelerated graphics, if available, for performance. Line 9 sets the attribute android:screenOrientation to specify that this app should always appear in landscape mode (that is, a horizontal orientation).


 1   <?xml version="1.0" encoding="utf-8"?>
 2   <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 3      android:versionCode="1" android:versionName="1.0"
 4      package="com.deitel.spoton">
 5      <uses-sdk android:minSdkVersion="12"/>
 6      <application android:icon="@drawable/icon"
 7         android:hardwareAccelerated="true" android:label="@string/app_name">
 8         <activity android:name=".SpotOn" android:label="@string/app_name"
 9            android:screenOrientation="landscape">
10            <intent-filter>
11               <action android:name="android.intent.action.MAIN" />
12               <category android:name="android.intent.category.LAUNCHER"/>
13            </intent-filter>
14         </activity>
15      </application>
16   </manifest>


Fig. 8.3. AndroidManifest.xml.

8.4.2. main.xml RelativeLayout

This app’s main.xml (Fig. 8.4) layout file contains a RelativeLayout that positions the app’s TextViews for displaying the high score, level and current score, and a LinearLayout for displaying the lives remaining. The layouts and GUI components used here have been presented previously, so we’ve highlighted only the key features in the file. Figure 8.5 shows the app’s GUI component names.


 1   <?xml version="1.0" encoding="utf-8"?>
 2   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3      android:id="@+id/relativeLayout" android:layout_width="match_parent"
 4      android:layout_height="match_parent"
 5      android:background="@android:color/white">
 6      <TextView android:id="@+id/highScoreTextView"
 7         android:layout_width="wrap_content"
 8         android:layout_height="wrap_content"
 9         android:layout_marginTop="10dp"
10         android:layout_marginLeft="10dp"
11         android:textColor="@android:color/black" android:textSize="25sp"
12         android:text="@string/high_score"></TextView>
13      <TextView android:id="@+id/levelTextView"
14         android:layout_toRightOf="@id/highScoreTextView"
15         android:layout_width="wrap_content"
16         android:layout_height="wrap_content"
17         android:layout_marginTop="10dp"
18         android:layout_marginRight="10dp"
19         android:gravity="right"
20         android:layout_alignParentRight="true"
21         android:textColor="@android:color/black" android:textSize="25sp"
22         android:text="@string/level"></TextView>
23      <TextView android:id="@+id/scoreTextView"
24         android:layout_below="@id/highScoreTextView"
25         android:layout_width="wrap_content"
26         android:layout_height="wrap_content"
27         android:layout_marginLeft="10dp"
28         android:textColor="@android:color/black" android:textSize="25sp"
29         android:text="@string/score"></TextView>
30      <LinearLayout android:id="@+id/lifeLinearLayout"
31         android:layout_alignParentBottom="true"
32         android:layout_width="match_parent"
33         android:layout_height="wrap_content"
34         android:layout_margin="10dp"></LinearLayout>
35   </RelativeLayout >


Fig. 8.4. SpotOn’s main.xml layout file.

Image

Fig. 8.5. SpotOn GUI component names.

8.4.3. untouched.xml ImageView for an Untouched Spot

This app’s untouched.xml (Fig. 8.6) layout file contains an ImageView that’s inflated and configured dynamically as we create each new spot in the game.


1   <?xml version="1.0" encoding="utf-8"?>
2   <ImageView xmlns:android="http://schemas.android.com/apk/res/android">
3   </ImageView>


Fig. 8.6. SpotOn’s untouched.xml ImageView for a new spot.

8.4.4. life.xml ImageView for a Life

This app’s life.xml (Fig. 8.7) layout file contains an ImageView that’s inflated and configured dynamically each time a new life is added to the screen during the game.


1   <?xml version="1.0" encoding="utf-8"?>
2   <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
3      android:src="@drawable/life"></ImageView>


Fig. 8.7. SpotOn’s life.xml layout file.

8.5. Building the App

The SpotOn game consists of two classes—SpotOn (Section 8.5.1) is the app’s main Activity and class SpotOnView (Section 8.5.1) defines the game logic and spot animations.

8.5.1. SpotOn Subclass of Activity

Class SpotOn (Fig. 8.8) overrides onCreate to configure the GUI. Lines 24–25 create the SpotOnView and line 26 adds it to the RelativeLayout at position 0—that is, behind all the other elements in the layout. SpotOnView’s constructor requires three arguments—the Context in which this GUI component is displayed (i.e., this Activity), a SharedPreferences object and the RelativeLayout (so that the SpotOnView can interact with the other GUI components in the layout). Chapter 5 showed how to read from and write to a named SharedPreferences file. In this app, we use the default one that’s associated with the Activity, which we obtain with a call to Activity method getPreferences.


 1   // SpotOn.java
 2   // Activity for the SpotOn app
 3   package com.deitel.spoton;
 4
 5   import android.app.Activity;
 6   import android.content.Context;
 7   import android.os.Bundle;
 8   import android.widget.RelativeLayout;
 9
10   public class SpotOn extends Activity
11   {
12      private SpotOnView view; // displays and manages the game
13
14      // called when this Activity is first created
15      @Override
16      public void onCreate(Bundle savedInstanceState)
17      {
18         super.onCreate(savedInstanceState);
19         setContentView(R.layout.main);
20
21         // create a new SpotOnView and add it to the RelativeLayout
22         RelativeLayout layout =
23            (RelativeLayout) findViewById(R.id.relativeLayout);
24         view = new SpotOnView(this, getPreferences(Context.MODE_PRIVATE),
25            layout);                                                      
26         layout.addView(view, 0); // add view to the layout
27      } // end method onCreate
28
29      // called when this Activity moves to the background
30      @Override
31      public void onPause()
32      {
33         super.onPause();
34         view.pause(); // release resources held by the View
35      } // end method onPause
36
37      // called when this Activity is brought to the foreground
38      @Override
39      public void onResume()
40      {
41         super.onResume();
42         view.resume(this); // re-initialize resources released in onPause
43      } // end method onResume
44   } // end class SpotOn


Fig. 8.8. Class SpotOn defines the app’s main Activity.

Overridden Activity methods onPause and onResume call the SpotOnView’s pause and resume methods, respectively. When the Activity’s onPause method is called, SpotOnView’s pause method releases the SoundPool resources used by the app and cancels any running animations. As you know when an Activity begins executing, its onCreate method is called. This is followed by calls to the Activity’s onStart then onResume methods. Method onResume is also called when an Activity in the background returns to the foreground. When onResume is called in this app’s Activity, SpotOnView’s resume method obtains the SoundPool resources again and restarts the game. This app does not save the game’s state when the app is not on the screen.

8.5.2. SpotOnView Subclass of View

Class SpotOnView (Figs. 8.98.21) defines the game logic and spot animations.


 1   // SpotOnView.java
 2   // View that displays and manages the game
 3   package com.deitel.spoton;
 4
 5   import java.util.HashMap;
 6   import java.util.Map;
 7   import java.util.Random;
 8   import java.util.concurrent.ConcurrentLinkedQueue;
 9   import java.util.Queue;                           
10
11   import android.animation.Animator;               
12   import android.animation.AnimatorListenerAdapter;
13   import android.app.AlertDialog;
14   import android.app.AlertDialog.Builder;
15   import android.content.Context;
16   import android.content.DialogInterface;
17   import android.content.SharedPreferences;
18   import android.content.res.Resources;
19   import android.media.AudioManager;
20   import android.media.SoundPool;
21   import android.os.Handler;
22   import android.view.LayoutInflater;
23   import android.view.MotionEvent;
24   import android.view.View;
25   import android.widget.ImageView;
26   import android.widget.LinearLayout;
27   import android.widget.RelativeLayout;
28   import android.widget.TextView;
29


Fig. 8.9. SpotOnView package and import statements.

package and import Statements

Section 8.3 discussed the key new classes and interfaces that class SpotOnView uses. We’ve highlighted them in Fig. 8.9.

Constants and Instance Variables

Figure 8.10 begins class SpotOnView’s definition and defines the class’s constants and instance variables. Lines 33–34 define a constant and a SharedPreferences variable that we use to load and store the game’s high score in the Activity’s default SharedPreferences file. Lines 37–73 define variables and constants for managing aspects of the game—we discuss these variables as they’re used. Lines 76–84 define variables and constants for managing and playing the game’s sounds. Chapter 7 demonstrated how to use sounds in an app.


30   public class SpotOnView extends View
31   {
32      // constant for accessing the high score in SharedPreference
33      private static final String HIGH_SCORE = "HIGH_SCORE";
34      private SharedPreferences preferences; // stores the high score
35
36      // variables for managing the game
37      private int spotsTouched; // number of spots touched
38      private int score; // current score
39      private int level; // current level
40      private int viewWidth; // stores the width of this View
41      private int viewHeight; // stores the height of this view
42      private long animationTime; // how long each spot remains on the screen
43      private boolean gameOver; // whether the game has ended
44      private boolean gamePaused; // whether the game has ended
45      private boolean dialogDisplayed; // whether the game has ended
46      private int highScore; // the game's all time high score
47
48      // collections of spots (ImageViews) and Animators
49      private final Queue<ImageView> spots =    
50         new ConcurrentLinkedQueue<ImageView>();
51      private final Queue<Animator> animators = 
52         new ConcurrentLinkedQueue<Animator>(); 
53
54      private TextView highScoreTextView; // displays high score
55      private TextView currentScoreTextView; // displays current score
56      private TextView levelTextView; // displays current level
57      private LinearLayout livesLinearLayout; // displays lives remaining
58      private RelativeLayout relativeLayout; // displays spots
59      private Resources resources; // used to load resources
60      private LayoutInflater layoutInflater; // used to inflate GUIs
61
62      // time in milliseconds for spot and touched spot animations
63      private static final int INITIAL_ANIMATION_DURATION = 6000;
64      private static final Random random = new Random(); // for random coords
65      private static final int SPOT_DIAMETER = 100; // initial spot size
66      private static final float SCALE_X = 0.25f; // end animation x scale
67      private static final float SCALE_Y = 0.25f; // end animation y scale
68      private static final int INITIAL_SPOTS = 5; // initial # of spots
69      private static final int SPOT_DELAY = 500; // delay in milliseconds
70      private static final int LIVES = 3; // start with 3 lives
71      private static final int MAX_LIVES = 7; // maximum # of total lives
72      private static final int NEW_LEVEL = 10; // spots to reach new level
73      private Handler spotHandler; // adds new spots to the game
74
75      // sound IDs, constants and variables for the game's sounds
76      private static final int HIT_SOUND_ID = 1;
77      private static final int MISS_SOUND_ID = 2;
78      private static final int DISAPPEAR_SOUND_ID = 3;
79      private static final int SOUND_PRIORITY = 1;
80      private static final int SOUND_QUALITY = 100;
81      private static final int MAX_STREAMS = 4;
82      private SoundPool soundPool; // plays sound effects
83      private int volume; // sound effect volume
84      private Map<Integer, Integer> soundMap; // maps ID to soundpool
85


Fig. 8.10. SpotOnView constants and instance variables.

SpotOnView Constructor

Class SpotOnView’s constructor (Fig. 8.11) initializes several of the class’s instance variables. Line 93 stores the SpotOn Activity’s default SharedPreferences object, then line 94 uses it to load the high score. The second argument indicates that getInt should return 0 if the key HIGH_SCORE does not already exist. Line 97 uses the context argument to get and store the Activity’s Resources object—we’ll use this to load String resources for displaying the current and high scores, the current level and the user’s final score. Lines 100–101 store a LayoutInflater for inflating the ImageViews dynamically throughout the game. Line 104 stores the reference to the SpotOn Activity’s RelativeLayout, then lines 105–112 use it to get references to the LinearLayout where lives are displayed and the TextViews that display the high score, current score and level. Line 114 creates a Handler that method resetGame (Fig. 8.14) uses to display the game’s first several spots.


86      // constructs a new SpotOnView
87      public SpotOnView(Context context, SharedPreferences sharedPreferences,
88         RelativeLayout parentLayout)
89      {
90         super(context);
91
92         // load the high score
93         preferences = sharedPreferences;
94         highScore = preferences.getInt(HIGH_SCORE, 0);
95
96         // save Resources for loading external values
97         resources = context.getResources();
98
99         // save LayoutInflater
100        layoutInflater = (LayoutInflater) context.getSystemService(
101           Context.LAYOUT_INFLATER_SERVICE);
102
103        // get references to various GUI components
104        relativeLayout = parentLayout;
105        livesLinearLayout = (LinearLayout) relativeLayout.findViewById(
106           R.id.lifeLinearLayout);
107        highScoreTextView = (TextView) relativeLayout.findViewById(
108           R.id.highScoreTextView);
109        currentScoreTextView = (TextView) relativeLayout.findViewById(
110           R.id.scoreTextView);
111        levelTextView = (TextView) relativeLayout.findViewById(
112           R.id.levelTextView);
113
114        spotHandler = new Handler(); // used to add spots when game starts
115     } // end SpotOnView constructor
116


Fig. 8.11. SpotOnView constructor.

Overriding View Method onSizeChanged

We use the SpotOnView’s width and height when calculating the random coordinates for each new spot’s starting and ending locations. The SpotOnView is not sized until it’s added to the View hierarchy, so we can’t get the width and height in its constructor. Instead, we override View’s onSizeChanged method (Fig. 8.12), which is guaranteed to be called after the View is added to the View hierarchy and sized.


117     // store SpotOnView's width/height
118     @Override
119     protected void onSizeChanged(int width, int height, int oldw, int oldh)
120     {
121        viewWidth = width; // save the new width
122        viewHeight = height; // save the new height
123     } // end method onSizeChanged
124


Fig. 8.12. Overriding View method onSizeChanged.

Methods pause, cancelAnimations and resume

Methods pause, cancelAnimations and resume (Fig. 8.13) help manage the app’s resources and ensure that the animations do not continue executing when the app is not on the screen.


125     // called by the SpotOn Activity when it receives a call to onPause
126     public void pause()
127     {
128        gamePaused = true;
129        soundPool.release(); // release audio resources
130        soundPool = null;
131        cancelAnimations(); // cancel all outstanding animations
132     } // end method pause
133
134     // cancel animations and remove ImageViews representing spots
135     private void cancelAnimations()
136     {
137        // cancel remaining animations
138        for (Animator animator : animators)
139           animator.cancel();              
140
141        // remove remaining spots from the screen
142        for (ImageView view : spots)
143           relativeLayout.removeView(view);
144
145        spotHandler.removeCallbacks(addSpotRunnable);
146        animators.clear();
147        spots.clear();
148     } // end method cancelAnimations
149
150     // called by the SpotOn Activity when it receives a call to onResume
151     public void resume(Context context)
152     {
153        gamePaused = false;
154        initializeSoundEffects(context); // initialize app's SoundPool
155
156        if (!dialogDisplayed)
157           resetGame(); // start the game
158     } // end method resume
159


Fig. 8.13. SpotOnView methods pause, cancelAnimations and resume.

When the Activity’s onPause method is called, method pause (lines 126–132) releases the SoundPool resources used by the app and calls cancelAnimations. Variable gamePaused is used in Fig. 8.18 to ensure that method missedSpot is not called when an animation ends and the app is not on the screen.

Method cancelAnimations (lines 135–148) iterates through the animators collection and calls method cancel on each Animator. This immediately terminates each animation and calls its AnimationListener’s onAnimationCancel and onAnimationEnd methods.

When the Activity’s onResume method is called, method resume (lines 151–158) obtains the SoundPool resources again by calling initalizeSoundEffects (Fig. 8.15). If dialogDisplayed is true, the end-of-game dialog is still displayed on the screen and the user can click the dialog’s Reset Game button to start a new game; otherwise, line 157 calls resetGame (Fig. 8.14) to start a new game.


160     // start a new game
161     public void resetGame()
162     {
163        spots.clear(); // empty the List of spots
164        animators.clear(); // empty the List of Animators
165        livesLinearLayout.removeAllViews(); // clear old lives from screen
166
167        animationTime = INITIAL_ANIMATION_DURATION; // init animation length
168        spotsTouched = 0; // reset the number of spots touched
169        score = 0; // reset the score
170        level = 1; // reset the level
171        gameOver = false; // the game is not over
172        displayScores(); // display scores and level
173
174        // add lives
175        for (int i = 0; i < LIVES; i++)
176        {
177           // add life indicator to screen
178           livesLinearLayout.addView(
179              (ImageView) layoutInflater.inflate(R.layout.life, null));
180        } // end for
181
182        // add INITIAL_SPOTS new spots at SPOT_DELAY time intervals in ms
183        for (int i = 1; i <= INITIAL_SPOTS; ++i)                    
184           spotHandler.postDelayed(addSpotRunnable, i * SPOT_DELAY);
185     } // end method resetGame
186


Fig. 8.14. SpotOnView method resetGame.


187     // create the app's SoundPool for playing game audio
188     private void initializeSoundEffects(Context context)
189     {
190        // initialize SoundPool to play the app's three sound effects
191        soundPool = new SoundPool(MAX_STREAMS, AudioManager.STREAM_MUSIC,
192           SOUND_QUALITY);
193
194        // set sound effect volume
195        AudioManager manager =
196           (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
197        volume = manager.getStreamVolume(AudioManager.STREAM_MUSIC);
198
199        // create sound map
200        soundMap = new HashMap<Integer, Integer>(); // create new HashMap
201
202        // add each sound effect to the SoundPool
203        soundMap.put(HIT_SOUND_ID,
204           soundPool.load(context, R.raw.hit, SOUND_PRIORITY));
205        soundMap.put(MISS_SOUND_ID,
206           soundPool.load(context, R.raw.miss, SOUND_PRIORITY));
207        soundMap.put(DISAPPEAR_SOUND_ID,
208           soundPool.load(context, R.raw.disappear, SOUND_PRIORITY));
209     } // end method initializeSoundEffect
210


Fig. 8.15. SpotOnView method initializeSoundEffects.

Method resetGame

Method resetGame (Fig. 8.14) restores the game to its initial state, displays the initial extra lives and schedules the display of the initial spots. Lines 163–164 clear the spots and animators collections, and line 165 uses ViewGroup method removeAllViews to remove the life ImageViews from the livesLinearLayout. Lines 167–171 reset instance variables that are used to manage the game:

animationTime specifies the duration of each animation—for each new level, we decrease the animation time by 5% from the prior level

spotsTouched helps determine when each new level is reached, which occurs every NEW_LEVEL spots

score stores the current score

level stores the current level

gameOver indicates whether the game has ended

Line 172 calls displayScores (Fig. 8.16) to reset the game’s TextViews. Lines 175–180 inflate the life.xml file repeatedly and add each new ImageView that’s created to the livesLinearLayout. Finally, lines 183–184 use spotHandler to schedule the display of the game’s first several spots every SPOT_DELAY milliseconds.


211     // display scores and level
212     private void displayScores()
213     {
214        // display the high score, current score and level
215        highScoreTextView.setText(
216           resources.getString(R.string.high_score) + " " + highScore);
217        currentScoreTextView.setText(
218           resources.getString(R.string.score) + " " + score);
219        levelTextView.setText(
220           resources.getString(R.string.level) + " " + level);
221     } // end function displayScores
222


Fig. 8.16. SpotOnView method displayScores.

Method initializeSoundEffects

Method initializeSoundEffects (Fig. 8.15) uses the techniques we introduced in the Cannon Game app (Section 7.5.3) to prepare the game’s sound effects. In this game, we use three sounds represented by the following resources:

R.raw.hit is played when the user touches a spot

R.raw.miss is played when the user touches the screen, but misses a spot

R.raw.disappear is played when a spot completes its animation without having been touched by the user

These MP3 files are provided with the book’s examples.

Method displayScores

Method displayScores (Fig. 8.16) simply updates the game’s three TextViews with the high score, current score and current level. Parts of each string are loaded from the strings.xml file using the resources object’s getString method.

Runnable AddSpotRunnable

When method resetGame (Fig. 8.14) uses spotHandler to schedule the game’s initial spots for display, each call to the spotHandler’s postDelayed method receives the addSpotRunnable (Fig. 8.17) as an argument. This Runnable’s run method simply calls method addNewSpot (Fig. 8.18).


223     // Runnable used to add new spots to the game at the start
224     private Runnable addSpotRunnable = new Runnable()
225     {
226        public void run()
227        {
228           addNewSpot(); // add a new spot to the game
229        } // end method run
230     }; // end Runnable
231


Fig. 8.17. Runnable addSpotRunnable adds a new spot to the game.


232     // adds a new spot at a random location and starts its animation
233     public void addNewSpot()
234     {
235        // choose two random coordinates for the starting and ending points
236        int x = random.nextInt(viewWidth - SPOT_DIAMETER);
237        int y = random.nextInt(viewHeight - SPOT_DIAMETER);
238        int x2 = random.nextInt(viewWidth - SPOT_DIAMETER);
239        int y2 = random.nextInt(viewHeight - SPOT_DIAMETER);
240
241        // create new spot
242        final ImageView spot =
243           (ImageView) layoutInflater.inflate(R.layout.untouched, null);
244        spots.add(spot); // add the new spot to our list of spots
245        spot.setLayoutParams(new RelativeLayout.LayoutParams(
246           SPOT_DIAMETER, SPOT_DIAMETER));
247        spot.setImageResource(random.nextInt(2) == 0 ?
248           R.drawable.green_spot : R.drawable.red_spot);
249        spot.setX(x); // set spot's starting x location
250        spot.setY(y); // set spot's starting y location
251        spot.setOnClickListener( // listens for spot being clicked
252           new OnClickListener()
253           {
254           public void onClick(View v)
255           {
256              touchedSpot(spot); // handle touched spot
257           } // end method onClick
258        } // end OnClickListener
259     ); // end call to setOnClickListener
260     relativeLayout.addView(spot); // add spot to the screen
261
262     // configure and start spot's animation
263     spot.animate().x(x2).y(y2).scaleX(SCALE_X).scaleY(SCALE_Y)         
264        .setDuration(animationTime).setListener(                        
265           new AnimatorListenerAdapter()                                
266           {                                                            
267              @Override                                                 
268              public void onAnimationStart(Animator animation)          
269              {                                                         
270                 animators.add(animation); // save for possible cancel  
271              } // end method onAnimationStart                          
272                                                                        
273              public void onAnimationEnd(Animator animation)            
274              {                                                         
275                 animators.remove(animation); // animation done, remove 
276                                                                        
277                 if (!gamePaused && spots.contains(spot)) // not touched
278                 {                                                      
279                    missedSpot(spot); // lose a life                    
280                 } // end if                                            
281              } // end method onAnimationEnd                            
282           } // end AnimatorListenerAdapter                             
283        ); // end call to setListener                                   
284  } // end addNewSpot method
285


Fig. 8.18. SpotOnView method addNewSpot.

Method addNewSpot

Method addNewSpot (Fig. 8.18) adds one new spot to the game. It’s called several times near the beginning of the game to display the initial spots and whenever the user touches a spot or a spots animation ends without the spot being touched.

Lines 236–239 use the SpotOnView’s width and height to select the random coordinates where the spot will begin and end its animation. Then lines 242–250 inflate and configure the new spot’s ImageView. Lines 245–246 specify the ImageView’s width and height by calling its setLayoutParams method with a new RelativeLayout.LayoutParams object. Next, lines 247–248 randomly select between two image resources and call ImageView method setImageResource to set the spot’s image. Lines 249–250 set the spot’s initial position. Lines 251–259 configure the ImageView’s OnClickListener to call touchedSpot (Fig. 8.20) when the user touches the ImageView. Then we add the spot to the relativeLayout, which displays it on the screen.

Lines 263–283 configure the spot’s ViewPropertyAnimator, which is returned by the View’s animate method. A ViewPropertyAnimator configures animations for commonly animated View properties—alpha (transparency), rotation, scale, translation (moving relative to the current location) and location. In addition, a ViewPropertyAnimator provides methods for setting an animation’s duration, AnimatorListener (to respond to animation lifecycle events) and TimeInterpolator (to determine how property values are calculated throughout the animation). To configure the animation, you chain ViewPropertyAnimator method calls together. In this example, we use the following methods:

x—specifies the final value of the View’s x-coordinate

y—specifies the final value of the View’s y-coordinate

scaleX—specifies the View’s final width as a percentage of the original width

scaleY—specifies the View’s final height as a percentage of the original height

setDuration—specifies the animation’s duration in milliseconds

setListener—specifies the animation’s AnimatorListener

When the last method call in the chain (setListener in our case) completes execution, the animation starts. If you don’t specify a TimeInterpolator, a LinearInterpolator is used by default—the change in values for each property over the animation’s duration is constant. For a list of the predefined interpolators, visit

developer.android.com/reference/android/animation/
   TimeInterpolator.html

For our AnimatorListener, we create an anonymous class that extends AnimatorListenerAdapter, which provides empty method definitions for each of AnimatorListener’s four methods. We override only onAnimationStart and onAnimationEnd here.

When the animation begins executing, its listener’s onAnimationStart method is called. The Animator that the method receives as an argument provides methods for manipulating the animation that just started. We store the Animator in our animators collection. When the SpotOn Activity’s onPause method is called, we’ll use the Animators in this collection to cancel the animations.

When the animation finishes executing, its listener’s onAnimationEnd method is called. We remove the corresponding Animator from our animators collection (it’s no longer needed). Then, if the game is not paused and the spot is still in the spots collection, we call missedSpot (Fig. 8.21) to indicate that the user missed this spot and should lose a life. If the user touched the spot, it will no longer be in the spots collection.

Overriding View Method onTouchEvent

Overridden View method onTouchEvent (Fig. 8.19) responds to touches in which the user touches the screen but misses a spot. We play the sound for a missed touch, subtract 15 times the level from the score, ensure that the score does not fall below 0 and display the updated score.


286     // called when the user touches the screen, but not a spot
287     @Override
288     public boolean onTouchEvent(MotionEvent event)
289     {
290        // play the missed sound
291        if (soundPool != null)
292           soundPool.play(MISS_SOUND_ID, volume, volume,
293              SOUND_PRIORITY, 0, 1f);
294
295        score -= 15 * level; // remove some points
296        score = Math.max(score, 0); // do not let the score go below zero
297        displayScores(); // update scores/level on screen
298        return true;
299     } // end method onTouchEvent
300


Fig. 8.19. Overriding View method onTouchEvent.

Method touchedSpot

Method touchedSpot (Fig. 8.20) is called each time the user touches an ImageView representing a spot. We remove the spot from the game, update the score and play the sound indicating a hit spot. Next, we determine whether the user has reached the next level and whether a new life needs to be added to the screen (only if the user has not reached the maximum number of lives). Finally, we display the updated score and, if the game is not over, add a new spot to the screen.


301     // called when a spot is touched
302     private void touchedSpot(ImageView spot)
303     {
304        relativeLayout.removeView(spot); // remove touched spot from screen
305        spots.remove(spot); // remove old spot from list
306
307        ++spotsTouched; // increment the number of spots touched
308        score += 10 * level; // increment the score
309
310        // play the hit sounds
311        if (soundPool != null)
312           soundPool.play(HIT_SOUND_ID, volume, volume,
313              SOUND_PRIORITY, 0, 1f);
314
315        // increment level if player touched 10 spots in the current level
316        if (spotsTouched % 10 == 0)
317        {
318           ++level; // increment the level
319           animationTime *= 0.95; // make game 5% faster than prior level
320
321           // if the maximum number of lives has not been reached
322           if (livesLinearLayout.getChildCount() < MAX_LIVES)
323           {
324              ImageView life =
325                 (ImageView) layoutInflater.inflate(R.layout.life, null);
326              livesLinearLayout.addView(life); // add life to screen
327           } // end if
328        } // end if
329
330        displayScores(); // update score/level on the screen
331
332        if (!gameOver)
333           addNewSpot(); // add another untouched spot
334     } // end method touchedSpot
335


Fig. 8.20. SpotOnView method touchedSpot.

Method missedSpot

Method missedSpot (Fig. 8.21) is called each time a spot reaches the end of its animation without having been touched by the user. We remove the spot from the game and, if the game is already over, immediately return from the method. Otherwise, we play the sound for a disappearing spot. Next, we determine whether the game should end. If so, we check whether there is a new high score and store it (lines 356–362). Then we cancel all remaining animations and display a dialog showing the user’s final score. If the user still has lives remaining, lines 385–390 remove one life and add a new spot to the game.


336     // called when a spot finishes its animation without being touched
337     public void missedSpot(ImageView spot)
338     {
339        spots.remove(spot); // remove spot from spots List
340        relativeLayout.removeView(spot); // remove spot from screen
341
342        if (gameOver) // if the game is already over, exit
343           return;
344
345        // play the disappear sound effect
346        if (soundPool != null)
347           soundPool.play(DISAPPEAR_SOUND_ID, volume, volume,
348              SOUND_PRIORITY, 0, 1f);
349
350        // if the game has been lost
351        if (livesLinearLayout.getChildCount() == 0)
352        {
353           gameOver = true; // the game is over
354
355           // if the last game's score is greater than the high score
356           if (score > highScore)
357           {
358              SharedPreferences.Editor editor = preferences.edit();
359              editor.putInt(HIGH_SCORE, score);
360              editor.commit(); // store the new high score
361              highScore = score;
362           } // end if
363
364           cancelAnimations();
365
366           // display a high score dialog
367           Builder dialogBuilder = new AlertDialog.Builder(getContext());
368           dialogBuilder.setTitle(R.string.game_over);
369           dialogBuilder.setMessage(resources.getString(R.string.score) +
370              " " + score);
371           dialogBuilder.setPositiveButton(R.string.reset_game,
372              new DialogInterface.OnClickListener()
373              {
374                 public void onClick(DialogInterface dialog, int which)
375                 {
376                    displayScores(); // ensure that score is up to date
377                    dialogDisplayed = false;
378                    resetGame(); // start a new game
379                 } // end method onClick
380              } // end DialogInterface
381           ); // end call to dialogBuilder.setPositiveButton
382           dialogDisplayed = true;
383           dialogBuilder.show(); // display the reset game dialog
384        } // end if
385        else // remove one life
386        {
387           livesLinearLayout.removeViewAt( // remove life from screen
388              livesLinearLayout.getChildCount() - 1);
389           addNewSpot(); // add another spot to game
390        } // end else
391     } // end method missedSpot
392  } // end class SpotOnView


Fig. 8.21. SpotOnView method missedSpot.

8.6. Wrap-Up

In this chapter, we presented the SpotOn game, which tested a user’s reflexes by requiring the user to touch moving spots before they disappear. This was our first app that used features specific to Android 3.0 or higher. In particular, we used property animation, which was introduced in Android 3.0, to move and scale ImageViews.

You learned that Android versions prior to 3.0 had two animation mechanisms—tweened View animations that allow you to change limited aspects of a View’s appearance and frame View animations that display a sequence of images. You also learned that View animations affect only how a View is drawn on the screen.

Next, we introduced property animations that can be used to animate any property of any object. You learned that property animations animate values over time and require a target object containing the property or properties to animate, the length of the animation, the values to animate between for each property and how to change the property values over time.

We discussed Android 3.0’s ValueAnimator and ObjectAnimator classes, then focused on Android 3.1’s new utility class ViewPropertyAnimator, which was added to the animation APIs to simplify property animation for Views and to allow animation of multiple properties in parallel.

We used a View’s animate method to obtain the View’s ViewPropertyAnimator, then chained method calls to configure the animation. When the last method call in the chain completed execution, the animation started. You listened for property-animation lifecycle events by implementing the interface AnimatorUpdateListener, which defines methods that are called when an animation starts, ends, repeats or is canceled. Since we needed only two of the lifecycle events, we implemented our listener by extending class AnimatorListenerAdapter.

Finally, you used the ConcurrentLinkedQueue class from package java.util.concurrent and the Queue interface to maintain thread-safe lists of objects that could be accessed from multiple threads of execution in parallel. In Chapter 9, we present the Doodlz app, which uses Android’s graphics capabilities to turn a device’s screen into a virtual canvas.

Self-Review Exercises

8.1. Fill in the blanks in each of the following statements:

a. __________ View animations display a sequence of images.

b. You can listen for property-animation lifecycle events by implementing the interface __________, which defines methods that are called when an animation starts, ends, repeats or is canceled.

c. A(n) __________ configures animations for commonly animated View properties—alpha (transparency), rotation, scale, translation and location.

d. Class ViewPropertyAnimator was added to Android 3.1 to simplify property animation for Views and to allow animation of multiple properties in __________.

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

a. The property animation class PropertyAnimator calculates property values over time, but you must specify an AnimatorUpdateListener in which you programmatically modify the target object’s property values.

b. Android 3.1 added the utility class ViewPropertyAnimator to simplify property animation for Views and to allow multiple properties to be animated in sequence.

c. When an Activity begins executing, its onCreate method is called. This is followed by calls to the Activity’s onPause then onResume methods. Method onResume is also called when an Activity in the background returns to the foreground.

Answers to Self-Review Exercises

8.1.

a. Frame.

b. AnimatorListener.

c. ViewPropertyAnimator.

d. parallel.

8.2.

a. False. The property animation class ValueAnimator calculates property values over time, but you must specify an AnimatorUpdateListener in which you programmatically modify the target object’s property values.

b. False. Android 3.1 added the new utility class ViewPropertyAnimator to simplify property animation for Views and to allow multiple properties to be animated in parallel.

c. False. When an Activity begins executing, its onCreate method is called. This is followed by calls to the Activity’s onStart then onResume methods. Method onResume is also called when an Activity in the background returns to the foreground.

Exercises

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

a. Property animations can be used to animate any property of any object.

b. You can use the ConcurrentLinkedQueue class from package java.util.concurrent and the Queue interface to maintain thread-safe lists of objects that can be accessed from multiple threads of execution in parallel.

8.4. Fill in the blanks in each of the following statements:

a. __________ View animations allow you to change limited aspects of a View’s appearance, such as where it’s displayed, its rotation and its size.

b. With __________ animation (package android.animation), you can animate any property of any object—the mechanism is not limited to Views.

c. ValueAnimator subclass __________ uses the target object’s set methods to modify the object’s animated properties as their values change over time.

d. You can use the ConcurrentLinkedQueue class (from package java.util.concurrent) and the Queue interface to maintain __________ lists of objects that can be accessed from multiple threads of execution in parallel.

e. Setting the attribute android:hardwareAccelerated to "true" allows the app to use hardware accelerated __________, if available, for performance.

f. In addition, a ViewPropertyAnimator provides methods for setting an animation’s duration, __________ (to respond to animation lifecycle events) and TimeInterpolator (to determine how property values are calculated throughout the animation).

8.5. (Enhanced SpotOn Game) Make the following enhancements to the SpotOn Game app:

a. Make the game more challenging by having spots flash on and off the screen at random sizes and for random durations.

b. Make the spots grow and shrink in size.

c. Make the spots move on zig-zag lines rather than straight lines.

d. Add new sounds for significant game events like reaching a new level, earning a new life and losing a life.

e. Add a different color bonus spot with a point value of 100 times the current level. The spot should appear briefly so it’s more difficult to touch than the other spots.

f. Add difficulty levels for easy, standard, difficult and impossible. You can vary the spot size, duration on the screen and number of spots for each difficulty level.

g. Save the top five scores in a SharedPreferences file. When the game ends load the top five scores and display an AlertDialog with the scores shown in descending order. If the user’s score is one of the top five, highlight that score by displaying an asterisk (*) next to it.

8.6. (Multiplayer Horse Race with SpotOn Game) Modify and enhance the Horse Race Game from Exercise 7.7. Replace the Cannon Game with SpotOn. Rather than splitting the bottom portion of the screen, have the two players compete in SpotOn in one area. Include spots in two colors—one color for each player. Touching a spot of the appropriate color moves the corresponding player’s horse.

8.7. (15 Puzzle App) Create an app that enables the user to play the game of 15. The game is played on a 4-by-4 board having a total of 16 slots. One slot is empty; the others are occupied by 15 tiles numbered 1 through 15 and randomly arranged. The user can move any tile next to the currently empty slot into that slot by touching the tile. The goal is to arrange the tiles into sequential order, row by row. Add a timer and provide a score based on the amount of time it takes the user to complete the puzzle. The faster the user completes the puzzle, the higher the score.

8.8. (Speed Touch Game App) Display the numbers 1–16 in random order in a four-by-four grid and place a timer at the top of the screen and a Start button at the bottom of the screen. When the user touches the button, a timer begins. The goal of the game is to tap the 16 numbers in the proper order (1, 2, 3, etc.) as quickly as possible. The timer should stop when the numbers have been touched in the correct order and the last number is touched. Keep track of the shortest times in a SharedPreferences file. Provide multiple levels with larger sets of numbers. Consider providing levels with non-sequential sets of values in which the user has to identify the series of numbers, then touch them in the correct sequence (e.g., multiples of 2, powers of 2, the Fibonacci series, etc.).

8.9. (Tic-Tac-Toe App) Create a Tic-Tac-Toe app that displays a 3-by-3 grid of blank ImageViews. Allow two human players. When the first player touches a blank ImageView, display an X image, and when the second player touches a blank ImageView, display an O image. If either player touches an occupied location, play a buzzer sound. After each move, determine whether the game has been won or is a draw. If you feel ambitious, modify your app so that the device makes the moves for one of the players. Also, allow the player to specify whether he or she wants to go first or second against the computer. If you feel ambitious, develop an app that will play three-dimensional Tic-Tac-Toe on a 4-by-4-by-4 board. If you’re not familiar with 3D graphics, represent the levels of the board as four two-dimensional 4-by-4 boards side-by-side.

8.10. (Memory Game App) Create an app that tests the user’s memory. Include a four-by-five grid of blank squares. When the user touches a square, a number is revealed. The user then touches another square trying to find a match with the same number. If the two numbers do not match, the squares are flipped back to the blank side. If the two numbers match, the user gets a point and the squares are removed from the screen.

8.11. (Memory Game App Enhancement) Modify the app from Exercise 8.10 to use card images. (We provided card images in the card_images folder with this book’s examples.)

8.12. (Jigsaw Puzzle Quiz App) Place an image of a famous person or landmark behind a jigsaw puzzle. Incorporate a word, math or trivia quiz. For each correct answer, a piece of the jigsaw puzzle is removed, revealing a portion of the image behind it. The goal is for the user to guess the person or landmark before all of the jigsaw pieces are removed.

8.13. (Eight Queens App) A puzzler for chess buffs is the Eight Queens problem in which the goal is to place eight queens on an empty chessboard so that no queen is “attacking” any other—that is, no two queens are in the same row, in the same column or along the same diagonal. Create an app that displays an 8-by-8 checkerboard of ImageViews. Randomly place one queen on the board, then allow the user to touch cells to indicate where the other seven queens should be placed. Provide Buttons for undoing one move at a time and for clearing the board. Play a buzzer sound when the user attempts an invalid move. Use the animation techniques you learned in this chapter to animate the queens onto the board when the user places each queen.

8.14. (Knight’s Tour App) One of the more interesting puzzlers for chess buffs is the Knight’s Tour problem, originally proposed by the mathematician Euler. Can the knight chess piece move around an empty chessboard and touch each of the 64 squares once and only once? The knight makes only L-shaped moves (two spaces in one direction and one space in a perpendicular direction). Thus, from a square near the middle of an empty chessboard, the knight can make eight different moves. Create an app that randomly places the knight on a chessboard and allows the user to attempt the knight’s tour. When the user touches a square, ensure that it is unoccupied and represents a valid move of the knight. Use animation to move the knight to each new valid square the user touches. As the night leaves a given square, display the number of the move in that square. For example, when the knight leaves the original square in which it was randomly placed, display a 1 in that square. When the knight leaves the square of the second move, display a 2, and so on. Provide Buttons for undoing one move at a time and for clearing the board. Play a buzzer sound when the user attempts an invalid move. A full tour occurs when the knight makes 64 moves, touching each square of the chessboard once and only once. A closed tour occurs when the 64th move is one move away from the square in which the tour started. Include a test for a closed tour.

8.15. (Checkers App) Create an app that displays an 8-by-8 checkerboard of ImageViews and allow two users to play checkers against one another. Provide tests to determine when the game has ended.

8.16. (Chess App) Create an app that displays an 8-by-8 checkerboard of ImageViews and allow two users to play chess against one another. [Note: The logic of this game is extremely complex. Consider investigating open source chess programs that you can adapt into an app.]

8.17. (Tablet Typing Tutor App) Typing quickly and correctly is an essential skill for working effectively with a smartphone or tablet. A problem with typing on such devices is that it’s not true “touch typing.” One of the reasons people feel that tablets cannot replace desktops is because keys are smaller and you can’t feel them. Many others feel mobile devices are the future of computing.

In this exercise, you’ll build an app that can help users learn to “touch type” (i.e., type correctly without looking at the tablet’s keyboard). The app should display sample text that the user should type and an EditText in which to type the sample text. Use a TextWatcher to be notified when the text in the EditText changes, then compare the text typed so far with the sample text. If the last character typed by the user is incorrect, play a buzzer sound and remove that character from the EditText.

A great way to help users learn the locations of every letter on the keyboard is to have them type pangrams—phrases that contain every letter of the alphabet at least once, such as “The quick brown fox jumps over the lazy dog.” You can find other pangrams on the web.

To make the app more interesting you could monitor the user’s accuracy. You could keep track of how many keystrokes the user types correctly and how many are typed incorrectly. You could also keep track of which keys the user is having difficulty with and display a report showing those keys.

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

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