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 ImageView
s with ViewPropertyAnimator
s.
• Respond to animation lifecycle events with an AnimatorListener
.
• Process click events for ImageView
s 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.
8.2 Test-Driving the SpotOn Game App
8.4 Building the App’s GUI and Resource Files
8.4.3 untouched.xml ImageView
for an Untouched Spot
8.4.4 life.xml ImageView
for a Life
8.5.1 SpotOn
Subclass of Activity
8.5.2 SpotOnView
Subclass of View
Self-Review Exercises | Answers to Self-Review Exercises | Exercises
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).
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.
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.]
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 ImageView
s. 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 View
s. 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 View
s 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
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.
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 OnClickListener
s 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.
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.
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>
main.xml RelativeLayout
This app’s main.xml
(Fig. 8.4) layout file contains a RelativeLayout
that positions the app’s TextView
s 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 >
untouched.xml ImageView
for an Untouched SpotThis 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>
life.xml ImageView
for a LifeThis 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>
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.
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
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.
SpotOnView
Subclass of View
Class SpotOnView
(Figs. 8.9–8.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
package
and import
StatementsSection 8.3 discussed the key new classes and interfaces that class SpotOnView
uses. We’ve highlighted them in Fig. 8.9.
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
SpotOnView
ConstructorClass 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 ImageView
s 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 TextView
s 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
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
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
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
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
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 ImageView
s 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 TextView
s. 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
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.
displayScores
Method displayScores
(Fig. 8.16) simply updates the game’s three TextView
s 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
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
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 Animator
s 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.
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
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
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
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 ImageView
s.
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 View
s 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.
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 View
s 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 View
s 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.
a. Frame.
b. AnimatorListener
.
c. ViewPropertyAnimator
.
d. parallel.
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 View
s 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.
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 View
s.
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 ImageView
s. 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 ImageView
s. 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 Button
s 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 Button
s 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 ImageView
s 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 ImageView
s 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.