The Android development framework provides us with a set of classes for drawing shapes and bitmaps, animating those shapes and bitmaps, and making sounds. In this chapter, we build a simple game where the user shoots from a cannon at a duck flying across the screen. We learn how to draw basic shapes such as a line, a circle, or a rectangle; drawing a bitmap from a file; playing a sound; and how to capture and respond to touch events. We also learn how to refresh the screen at a given frequency so we can animate objects on the screen.
The android.graphics
package contains classes to draw and paint. TABLE 8.1 shows some of its selected classes.
Typically, we draw on a custom View (i.e., a user-defined subclass of the View
class). Drawing takes place in the onDraw
method, inherited from the View
class. For this, we can use the following template:
public class CustomView extends View { ... public void onDraw( Canvas canvas ) { super.onDraw( canvas ); // as needed, define style and color to draw with using a Paint object // call some drawing methods of the Canvas class with canvas } ...
TABLE 8.1 Selected classes of the android.graphics
package
Class | Description |
---|---|
Bitmap | Encapsulates a bitmap. |
BitmapFactory | Factory class to create Bitmap objects from various sources. |
Camera | Generates 3D transformations that can be applied to a Canvas. |
Canvas | Draws shapes and bitmaps. |
Color | Defines constants and methods for defining colors represented by integers. |
Paint | Defines style and color for drawing. |
Picture | Records drawing calls. |
Point | A point defined by two integer coordinates. |
Rect | A rectangle defined by integer coordinates. |
TABLE 8.2 Selected methods of the Paint
class
Method | Description |
---|---|
void setARGB( int a, int r, int g, int b ) | Sets the alpha, red, green, and blue color components of this Paint object to values a, r, g, and b (all between 0 and 255). |
void setColor( int color ) | Sets the color of this Paint object to color (including the alpha component). |
void setStrokeWidth( float width ) | Sets the stroke width of this Paint object to width. |
void setTextSize( float textSize ) | Sets the text size of this Paint object to textSize. |
void setStyle( Paint.Style style ) | Sets the style of this Paint object to style. Paint.Style is an enum whose possible values are STROKE, FILL, and FILL_AND_STROKE. The default is FILL. |
void setAntiAlias( boolean flag ) | Sets this Paint to use anti-alias when drawing shapes if flag is true, default is false. |
To specify how we draw (i.e., define the drawing style and color), we use the Paint
class. TABLE 8.2 shows some of its methods.
For example, to define a Paint
object and set its color to red and its stroke width to 5
pixels, we can use the following code sequence:
Paint paint = new Paint( ); paint.setColor( 0xFFFF0000 ); paint.setStrokeWidth( 5.0f );
To draw, we use the Canvas
class. It provides the tools to draw basic shapes and bitmaps previously generated, for example, from a file containing a picture, such as a jpeg. TABLE 8.3 shows some of its methods.
Assuming we already have declared, instantiated, and defined a Paint
object named paint
, here is how we can draw a circle inside the onDraw
method of the View
class so that it is centered at the point (50, 100)
and has a radius of 25
.
// canvas is the Canvas parameter of the onDraw method canvas.drawCircle( 50, 100, 25, paint );
The default style for Paint
is that shapes are filled when they are drawn. If we do not want shapes to be filled, we can set the style of the Paint
object to STROKE
(see Table 8.2) as in the following statement:
// paint is a Paint reference paint.setStyle( Paint.Style.STROKE );
Table 8-3 Selected methods of the Canvas
class
Method | Description |
---|---|
void drawLine( float startX, float startY, float endX, float endY, Paint paint ) | Draws a line between the points (startX, startY) and (endX, endY) using the style and color defined in paint. |
void drawLines ( float [ ] points, Paint paint ) | Draws lines defined in points using the style and color defined in paint. Each line is defined as four consecutive values in points. |
void drawOval( RectF rect, Paint paint ) | Draws an oval within the rectangle rect using the style and color defined in paint. RectF is similar to Rect, but uses floats instead of ints. |
void drawCircle( float centerX, float centerY, float radius, Paint paint ) | Draws a circle whose center is the point (centerX, centerY) and whose radius is radius using the style and color defined in paint. |
void drawPicture( Picture picture, Rect dst ) | Draws picture and stretches it to fit into the dst rectangle. |
void drawBitmap( Bitmap bitmap, Rect src, Rect dst, Paint paint ) | Draws the src rectangle from bitmap inside the dst rectangle using paint. If src is null, then the whole bitmap is drawn. The bitmap is drawn to the dimensions of dst. |
void drawRect( Rect rect, Paint paint ) | Draws the rectangle rect using the style and color defined in paint. |
void drawText( String text, float x, float y, Paint paint ) | Draws the String text starting at coordinates (x, y) using the style, including alignment, and color defined in paint. |
We can also use the Canvas
class to draw some graphics from a file. If we have a file named duck.png
, we should place that file in the drawable
directory of our project. Once a file is in the drawable
directory, it is a resource that we can refer to using R.drawable.nameOfFile
. In this case, we can refer to it using R.drawable.duck
. Note that we do not include the file extension.
First, we instantiate a Bitmap
object for that file. Then, we draw it with the canvas
parameter of the onDraw
method. In order to create a Bitmap
, we can use one of the many static
methods of the BitmapFactory
class. TABLE 8.4 lists some of them. Once we have a Bitmap
reference, we can retrieve or set some of its characteristics with various methods of the Bitmap
class. TABLE 8.5 lists some of them.
Assuming we have a file named duck.jpg
in the drawable
directory of our project, we can use the following code to draw it inside our custom View inside a rectangle defined by the following code sequence:
// duck.png is in the drawable directory Bitmap duck = BitmapFactory.decodeResource( getResources( ), R.drawable.duck ); Rect rect = new Rect( 20, 50, 20 + duck.getWidth( ), 50 + duck.getHeight( ) ); // canvas is the Canvas parameter of the onDraw method // paint is a Paint object canvas.drawBitmap( duck, null, rect, paint );
Note that the last two parameters of the Rect
constructor used previously are the right and bottom edges of the rectangle, not its width and height.
TABLE 8.4 Selected methods of the BitmapFactory
class
Method | Description |
---|---|
static Bitmap decodeResource( Resources res, int id ) | Creates and returns a Bitmap from the resource whose id is id that is contained in the Resources object res. |
static Bitmap decodeFile( String pathName ) | Creates and returns a Bitmap from the file pathName. |
static Bitmap decodeStream( InputStream is ) | Reads is and creates and returns a Bitmap from it. |
TABLE 8.5 Selected methods of the Bitmap
class
Method | Description |
---|---|
int getWidth( ) | Returns the width of this Bitmap. |
int getHeight( ) | Returns the height of this Bitmap. |
int getPixel( int x, int y ) | Returns the color, as an integer, at the (x, y) coordinate of this Bitmap; x and y must be greater than or equal to 0 and less than the width and the height of this Bitmap, respectively. |
void setPixel( int x, int y, int color ) | Sets to color the color of the pixel of this Bitmap located at the (x, y) coordinate. The same constraints as in the getPixel method apply. |
We use the Empty Activity template for this app. In Version 0, we display the cannon on the lower left corner of the screen and a duck at the top right corner of the screen. As in many games, and to keep the example simple, we only allow the game to be played in horizontal position. Thus, we add the following inside the activity
element of the AndroidManifest.xml file:
android:screenOrientation=”landscape”
To bring a jpeg or png file into our project, we copy it and paste it in the drawable
directory. The name of our file is duck.png
.
In order to subclass the View
class, we must provide a constructor that overrides a constructor of the View
class and call the super
constructor with its first statement. Otherwise, our code will not compile. Note that the View
class does not supply a default constructor. TABLE 8.6 shows some constructors and methods of the View
class.
TABLE 8.6 Selected constructors and methods of the View
class
Constructor | Description |
---|---|
View( Context context ) | Use this constructor when creating a View by code. |
View( Context context, AttributeSet attrs ) | Called when constructing a View by inflating an xml resource; attrs are attributes that are specified in the xml file. |
View( Context context, AttributeSet attrs, int defStyle ) | Called when constructing a View by inflating an xml resource; attrs are attributes that are specified in the xml file, and defStyle is the default style to be applied to the View created. |
Method | Description |
void onFinishInflate( ) | Called after a View has finished inflating from its xml resource. |
void onAttachedToWindow( ) | Called when this View is attached to its window. |
void onMeasure( int widthMeasuredSpec, int heightMeasuredSpec ) | Called to measure this View and its contents in order to determine the width and height of this View. |
void onSizeChanged( int width, int height, int oldWidth, int oldHeight ) | Called when the size of this View is changed. |
void onLayout( boolean changed, int left, int top, int right, int bottom ) | Called when this View assigns positions and dimensions to its children. The parameters relate to this View relative to its parent. |
void onDraw( Canvas canvas ) | Called when the View draws its contents. |
When a View
is set as the content View of an activity, the following methods of the View
class are automatically called, several times for some: onAttachedToWindow
, onMeasure
, onSizeChanged
, onLayout
, onDraw
; onFinishInflate
is only called if the View is inflated from an xml file. If we add all these methods to this example with an output statement to Logcat inside, the output in Logcat is as follows:
Inside GameView constructor Inside onAttachedToWindow Inside onMeasure Inside onMeasure Inside onSizeChanged Inside onLayout Inside onMeasure Inside onLayout Inside onDraw
EXAMPLE 8.1 shows the GameView
class, which inherits from View
. Later in this chapter, we expect the onDraw
method to be called often as we redraw the screen at a high frequency in order to update the game. Thus, we want to avoid declaring variables inside onDraw
so that our code is as efficient as possible. At lines 12–16, we declare various instance variables to store resources for the duck: a Paint
object to define styles and colors for drawing, a Bitmap
for the duck (duck
), a rectangle in which we draw the duck within the View (duckRect
), and the height of the screen.
The constructor is coded at lines 18–31. We expect to instantiate a GameView
object from an Activity
class. Thus, the constructor includes two integer parameters representing the width and height available within that activity. We assign the height
parameter to the instance variable height
at line 20 and initialize duck
at line 21. We store height in an instance variable because we need to access it in the onDraw
method. At lines 23–25, we set the coordinates of duckRect
in relation to the width and height values passed by the activity to the constructor. We position the rectangle in the top right corner of the screen, set its width to 1/5 of width
, and set its height so that we keep the same proportions of the original duck image. Thus, we assign width – width / 5 to its left edge coordinate and width
to its right edge coordinate. We want to set the height of our duck to change by the same scaling factor as the width does. We can easily compute the scaling factor as follows:
new duck width = width / 5 = old duck width * scale
Thus,
scale = width/( 5 * old duck width )
We assign the preceding value to the scale
variable at line 23. Be careful when performing floating point division: we want the rounding to an integer dimension to take place at the very end in order to minimize the loss of precision.
We initialize paint
at line 27, set its color to black at line 28, the anti-alias option to true
at line 29, and the stroke width of paint
to 10
at line 30.
The onDraw
method, at lines 33–43, is also called automatically, after onAttachedToWindow
, onMeasure
, onSizeChanged
, and onLayout
are called. After calling its super
method at line 34, we successively draw the cannon base at lines 35–36, draw the cannon barrel at lines 38–39, and draw the duck at lines 41–42. We draw the cannon using the drawCircle
method of the Canvas
class. The first two parameters are the x- and y-coordinates of the center of the circle, the third parameter is the radius of the circle, and the fourth parameter is a Paint
object defining the style and color of the circle. To draw a circle whose center is (0
, height
) and whose radius is 50
, we use this statement:
canvas.drawCircle( 0, height, 50, paint );
The coordinate (0
, height
) denotes the bottom left corner of the View. The style attributes of paint
are used to draw the circle. Because of the position of its center, only the top right quarter of the circle is visible.
We actually want to size the radius (50 above) relative to the size of the View. We use a 10% ratio for this. Thus, we draw the cannon base at line 36 using this statement:
canvas.drawCircle( 0, height, height / 10, paint );
Note that since we only draw a quarter of a circle, we could use the drawArc
method instead. It has the following API:
public void drawArc( RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint )
The RectF
class encapsulates a rectangle using float coordinates. We can create a RectF
using the following constructor:
public RectF( float left, float top, float right, float bottom )
The two angle parameters of drawArc
are in degrees: startAngle
specifies the starting angle: 0 specifies three o’clock. If sweepAngle
is positive, the arc is drawn clockwise, otherwise it is drawn counterclockwise. If useCenter
is true
, we draw a wedge, going through the center of the oval (the center of the circle if the RectF
argument is a square). Here is how we could draw the cannon base using drawArc
:
RectF cannonRect = new RectF( - height / 10, height – height / 10, height / 10, height + height / 10 ); canvas.drawArc( cannonRect, 0.0f, -90.0f, true, paint );
At line 39, we draw the cannon barrel using the drawLine
method of the Canvas
class. The first two parameters are the x- and y-coordinates of one end of the line, and the next two parameters are the x- and y-coordinates of the other end of the line. The fifth parameter is a Paint
object defining the style and color of the line. If we want to draw a line whose end points are (0
, height
) and (100
, height
– 100
) so that it is at a 45-degree angle, we use this statement:
canvas.drawLine( 0, height, 100, height - 100, paint );
Again, we want to size the length of the cannon barrel relative to the size of the View. We use a 20% ratio for this (note that the 20% ratio is along the x- and y-axis whereas the cannon barrel is at a 45-degree angle and therefore longer. Part of the barrel is hidden by the cannon base). Thus, we draw the cannon barrel at line 39 using this statement:
canvas.drawLine( 0, height, height / 5, height – height / 5, paint );
Later in the chapter, we will allow the user to change the angle of the cannon by touching the screen.
At line 42, we draw the duck using one of the drawBitmap
methods of the Canvas
class. The first parameter is a Bitmap
reference, the second parameter defines the rectangle to draw within the bitmap, the third parameter defines the rectangle where to perform the drawing on the View, and the fourth parameter is a Paint
object defining the style and color of the drawn bitmap. We draw the entire Bitmap duck
within the rectangle duckRect
, using this statement:
canvas.drawBitmap( duck, null, duckRect, paint );
The value null
for the second argument means that we are drawing the entire bitmap. The duck is drawn within the rectangle represented by the third argument, duckRect
, defined inside the onLayout
method.
EXAMPLE 8.2 shows the MainActivity
class. Because we want to have as big a screen as possible, we extend the Activity
class, not the AppCompatActivity
class. In this way, the screen does not include the action bar and is a little bigger. Instead of inflating an XML resource, it sets its content View to a GameView
. At line 10, we declare a GameView
instance variable, gameView
. It is instantiated inside onCreate
at line 26. We set the content View of the activity to gameView
at line 27.
We retrieve the size of the screen at lines 24–25 in order to pass its width and height to the GameView
constructor. However, the height of the screen includes the height of the status bar. Thus, we retrieve it at lines 16–22 and subtract it from the height of the screen at line 26 and pass the resulting value to the GameView
constructor. Appendix A explains in greater detail how to retrieve the heights of the status and action bars.
FIGURE 8.1 shows the Duck Hunting Game app, Version 0, running inside the emulator. The circle we drew for the cannon is smooth without ragged edges, as a result of using the anti-alias feature.
In a game, there can be many shapes and objects on the screen, moving, changing, and colliding. The functionality of a game can be quite complex and it is easier and cleaner to encapsulate it inside a Model. Our Model will reflect the state of the game and its rules. We have three objects in this game: the cannon, the bullet, and the duck. In order to keep things simple, the user can only shoot one bullet at a time from the cannon. If the bullet hits the duck or goes outside the screen, then the user can shoot again. Thus, there is either no or one bullet on the screen at all times. We can define the state of the game as follows:
▸ Overall game parameters: size (width and height), frequency of updates
▸ Cannon: position, angle
▸ Bullet: fired or not, size, position, velocity
▸ Duck: position, size, velocity, shot or flying
To keep things simple, the duck is expected to fly right to left, and the cannon is expected to be at the lower bottom of the screen. Those constraints are enforced in the Game
class.
The values we use for drawing purposes inside the GameView
class, such as the position of the duck and the cannon, as well as the dimensions of the game, should be retrieved from the Model.
We also need some functionality to capture what happens during the game:
▸ Start the game.
▸ Move the duck.
▸ Move the bullet.
▸ Test if the duck is outside the screen.
▸ Test if the bullet is outside the screen.
▸ Manage the state of the bullet (fired or not fired).
▸ Test if the duck was shot.
We encapsulate the Model in the Game
class, shown in EXAMPLE 8.3. We choose to have a lot of instance variables (lines 8–29) instead of constants to store the game parameters, such as the duck or the bullet velocity. The advantage of having instance variables over constants is that their value can be modified as the user plays. If we want to implement various game levels with increasing difficulty, we can, for example, increase the speed of the duck or the bullet, or reduce the size of the bullet. Note that we have an instance variable for the cannon angle and another one for the bullet angle. When the bullet is fired, we want the user to be able to modify the angle of the cannon for the next shot without modifying the angle of the bullet that has been fired.
A lot of the accessor methods are needed because when we need to redraw the corresponding View
, we need to access the data related to the various objects that we draw. The setCannon
method (lines 90–102) sets not only the position of the cannon, but also the original position of the bullet inside the cannon.
From lines 137 to 199, we code the various methods that enable us to change the state of the game, move the duck, move the bullet, test if the duck or the bullet has gone outside the screen (outside the hunting rectangle in this Model), reload the bullet in its original position, change the angle at which the bullet is fired, and test if the bullet hits the duck. The instance variable duckShot
represents whether the duck has been shot. The method duckHit
tests if the bullet hit the duck. If the duck is not shot, the moveDuck
method (lines 161–169) moves the duck from right to left. If the duck is shot, it moves it down at five times its flying speed.
TABLE 8.7 Selected intersects
method of the Rect
class
Method | Description |
---|---|
boolean intersects( int left, int top, int right, int bottom ) | Returns true if this Rect intersects the rectangle defined by the four parameters, false if it does not. This Rect is not modified. |
In order to test if the bullet hit the duck (method duckHit
at lines 195–199), we compare the rectangle around the duck to the rectangle around the bullet and test if they intersect. We use one of the intersects
methods of the Rect
class, shown in TABLE 8.7. It returns true
if the rectangle defined by its four parameters intersects with the Rect
reference calling the method, false
if it does not.
Now we can use the Model to make the duck fly across the screen from right to left. In order to do that, we need to redraw the View at a certain frequency. The human eye thinks that things move in continuous motion when the frame rate is at 20–30 frames per second or more. The higher the frame rate, the more things will look like they move in continuous motion. However, the higher the frame rate, the more demand there is on the CPU. If we ask the CPU to do too much in too short a period of time, movement will start to be jerky and the quality of our app will deteriorate. Furthermore, the CPU speed varies from device to device, which complicates things further. For complex games with a lot of objects moving and interacting with each other, it is recommended to use OpenGL (Open Graphics Library) so that drawings are rendered faster. OpenGL defines an API so that developers can interact directly with the graphics processor. Often, game developers use a game engine, which uses OpenGL, in order to develop a game. High performance game programming is beyond the scope of this book. We keep our game simple so that the CPU can handle it.
TABLE 8.8 Selected methods of the Timer
class
Method | Description |
---|---|
void schedule( TimerTask task, long delay, long period ) | Schedules task to run after delay milliseconds and every period milliseconds afterward. |
void cancel( ) | Cancels the scheduled tasks of this Timer. |
TABLE 8.9 Selected methods of the TimerTask
class
Method | Description |
---|---|
void run( ) | Called automatically with the specified frequency; override this method and execute the task here. |
boolean cancel( ) | Cancels this TimerTask. |
The Timer
class, from the java.util
package, provides several schedule
and scheduleAtFixedRate
methods to set up a given task, of type TimerTask
, to be performed either once or at a specified frequency, starting after a specified delay. We can also cancel tasks that are currently scheduled by a Timer
. TABLE 8.8 shows a schedule
method and the cancel
method.
The first parameter of the various schedule
and scheduleAtFixedRate
methods is a TimerTask
object. The TimerTask
class implements
the Runnable
interface and is also from the java.util
package. It is abstract
and meant to be overridden in order to define a custom task to be performed. When we subclass TimerTask
, we should provide a constructor and override the run
method. The run
method is automatically called at the specified frequency and executes the task. TABLE 8.9 shows the run
and cancel
methods.
We define a GameTimerTask
class that extends
the TimerTask
class. Inside the run
method, we update the state of the game using the Model. Before exiting the run
method, we force the View to be redrawn so that what the user sees on the screen reflects the state of the game at that time. For this, we need a reference to both the game and the View inside the GameTimerTask
. There are several methods that can be used to redraw a View. TABLE 8.10 lists some of them. The methods with four parameters enable us to redraw the rectangle of the View defined by these four parameters. If we know that only part of the View changes, this can save valuable CPU time. The postInvalidate
method of the View class automatically calls onDraw
and can be called from a non-user interface thread, which is the case when we execute inside the run
method of the GameTimerTask
. Thus, at the end of the run
method, we call postInvalidate
in order to redraw the View.
TABLE 8.10 Methods of the View
class that force a call to onDraw
Method | Description |
---|---|
void invalidate( ) | Refreshes this whole View by calling onDraw. This method must be called from a user-interface thread. |
void postInvalidate( ) | Refreshes this whole View by calling onDraw. This method can be called from outside a user-interface thread. |
void invalidate( int top, int left, int right, int bottom ) | Refreshes the rectangle within this View defined by the four parameters by calling onDraw. This method must be called from a user-interface thread. |
void postInvalidate( int top, int left, int right, int bottom ) | Refreshes the rectangle within this View defined by the four parameters by calling onDraw. This method can be called from outside a user-interface thread. |
The overall logic is as follows:
// Inside the MainActivity class Timer timer = new Timer( ); // Start the task now, run it 10 times per second (every 100 milliseconds) timer.schedule( new GameTimerTask( this ), 0, 100 ); // Inside the run method of the GameTimerTask class // 1 - Update the state of the game with a Game reference to our game // 2 – Assuming that gameView is a reference to our View, // call postInvalidate to force a call to onDraw gameView.postInvalidate( ); // Inside the onDraw method of GameView // Draw the cannon, bullet, and duck based on the state of game
To summarize:
▸ The run
method of the GameTimerTask
class asks the Model to update the state of the game, and then asks the game View to redraw itself.
▸ The onDraw
method of the GameView
class updates the View based on the state of the game.
EXAMPLE 8.4 shows the GameTimerTask
class. We import TimerTask
at line 3 and the class extends
it at line 5. We declare two instance variables (lines 6–7), game
, a Game
reference to our game, and gameView
, a GameView
reference to our View. We use game
to start the game at line 12 and update the state of the game at lines 16–18 inside the run
method. We use gameView
to call postInvalidate
at line 19, in order to trigger a call to the onDraw
method of GameView
.
We provide a constructor that accepts a GameView
parameter, view
, at lines 9–13; view
is assigned to gameView
at line 10. We call the getGame
accessor (we need to add the getGame
method to the GameView
class) at line 11 in order to assign a reference to our game to game
. We expect a GameTimerTask
object to be instantiated from inside the GameView
class, passing this
to the constructor.
EXAMPLE 8.5 shows the GameView
class. Instead of displaying a simple duck, we animate the duck when it is flying using three Bitmaps
that we create from three transparent png files. We place the three files anim_duck0.png
, anim_duck1.png
, and anim_duck2.png
in the drawable
directory of our project. We intend to use these three files to create a four frames animation (0-1-2-1) and store these drawable resources in the array TARGETS
(lines 14–15). There are four elements in the array in this example, but we could easily add more frames to improve the duck animation. Instead of one Bitmap
, we now have an array of Bitmaps
, ducks
, declared at line 17. The instance variable duckFrame
(line 18) stores the current index within the array ducks
. We use it to access the correct Bitmap
to draw inside the onDraw
method. We declare a Game
instance variable, game
, at line 20. Usually, we include a reference to the Model inside the Controller (i.e., the MainActivity
class). In this case, we set several parameters of what we draw inside the onDraw
method (cannon base and barrel, duck, bullet) based on values from the Model. Thus, it is convenient to have a Game
reference inside GameView
so that we can access the values of the various instance variables of Game
inside the onDraw
method.
The array ducks
parallels the array TARGETS
and is instantiated at line 24 and filled with Bitmaps
generated from the elements in TARGETS
at lines 25–27.
We assume that all the png files have the same width and height and use the first one to construct a Rect
at lines 29–30, after computing the scaling factor at line 28. We pass that Rect
to the Game
constructor at line 31 when we instantiate game
. We also want to set the duck and bullet speeds dynamically. Indeed, the duck and the bullet should move faster if the screen has higher resolution. If the screen is 1,000 pixels wide, then the duck speed is .03 pixel per second and the bullet speed is 10 times faster, .3 pixel per second. After setting the game’s hunting rectangle at line 36, we call setCannon
at lines 37–38 in order to set the dimensions of the cannon: we position the cannon at the left bottom corner of the hunting rectangle and size it relative to the size of the View in order to be as device-independent as possible.
We want our screen to be refreshed at about 10 times per second for this game. Thus, we define the constant DELTA_TIME
at line 13 and assign to it the value 100
. We pass that value to game
at line 34 and set the dimension of the cannon barrel at line 43.
The onDraw
method is coded at lines 46–64. We draw the cannon at lines 48–50, retrieving its center coordinates and radius from game
. We draw the cannon barrel at lines 52–59, also retrieving its coordinates, dimensions, and angle from game
. Before drawing the Bitmap
for the duck at line 63, we update the value of duckFrame
at line 62, so that we access the next Bitmap
in the array ducks
.
We provide an accessor for game
at lines 66–68. We call inside the GameTimerTask
of Example 8.6.
EXAMPLE 8.6 shows the edits in the MainActivity
class. We instantiate a Timer
object at line 30, and schedule a GameTimerTask
to run 10
times per second, starting immediately, at lines 31–32.
FIGURE 8.2 shows the Duck Hunting Game app, Version 1, running inside the emulator, with the duck flying in the middle of the screen. Note that the emulator has significantly less power than an actual device, and it is possible that it would not be able to draw the ducks at the specified frame rate.
In Version 2, we allow the user to move the cannon barrel using a single tap or swiping the screen. Wherever the user touches the screen, we point the cannon barrel toward that point. We also enable shooting by double tapping anywhere on the screen. However, we do not want a double tap to change the angle of the cannon barrel. In order to implement that, we need to capture the touch event and handle it as either a confirmed single tap or a swipe, moving the cannon barrel, or as a double tap, shooting. If it is a confirmed single tap or a swipe, we retrieve the x- and y-coordinates of the touch, and set the value of the cannon angle in game
. The redrawing of the cannon barrel should happen automatically since we take into account the value of the cannon barrel angle when we draw it inside the onDraw
method. If it is a double tap, we call the fireBullet
method with game
.
The easiest way to implement this is to use a GestureDetector
inside the GameView
class, create a private
class that extends the GestureDetector.SimpleOnGestureListener
class, and override the following methods: onSingleTapConfirmed
, onScroll
, and onDoubleTapEvent
.
Generally, events should be handled inside the Controller. Thus, we add all the event-related code to the MainActivity
class. EXAMPLE 8.7 shows the new parts of the MainActivity
class. At lines 7 and 8, we import the GestureDetector
and MotionEvent
classes. A GestureDetector
instance variable, detector
, is declared at line 14. Because we need to access and update the Model (changing the cannon angle, fire the bullet, etc.) based on user interaction, it is convenient to have a Game
instance variable (game
at line 15). The game
instance variable references the same Game
object as the game
instance variable of the GameView
class (line 38).
We code the private
class, TouchHandler
, extending GestureDetector.Simple OnGestureListener
at lines 49–74. Whether we execute inside the onSingleTapConfirmed
or onScroll
methods, we want to execute the same code, so we create a separate method, updateCannon
, and call it, passing the MouseEvent
parameter. Inside updateCannon
, coded at lines 68–73, we calculate the x- and y-coordinates of the touch relative to the center of the cannon at lines 69–70, and use the atan2
method of the Math
class to calculate the angle at line 71. We then call the setCannonAngle
method with game
and pass that calculated angle at line 72. To retrieve the touch coordinates, we use the getX
and getY
methods because they give us the x- and y-coordinates relative to the View, as opposed to the getRawX
and getRawY
methods that give us the absolute x- and y-coordinates.
Inside the onCreate
method, we instantiate detector
at line 40, passing th
, a TouchHandler
declared and instantiated at line 39, as its second argument. Then, we call the setOnDoubleTapListener
method at line 41, passing th
. Thus, gestures and touches trigger execution of the methods inside the TouchHandler
class, assuming that event dispatching has been set up inside onTouchEvent
. Inside the onTouchEvent
method (lines 44–47), we call the onTouchEvent
method of the GestureDetector
class with detector
so that the various touch events are dispatched to the appropriate methods among the nine methods of the TouchHandler
class (three overridden inside the class and six do-nothing methods inherited from GestureDetector.SimpleOnGestureListener
).
The only change in the GameView
class is to draw the bullet. This is done inside the onDraw
method. EXAMPLE 8.8 shows the updated onDraw
method. If the bullet is not off the screen (line 62), we draw it at lines 63–64 using its position and radius stored in the instance variable game
. As the game is played, the position of the bullet is updated every 100 ms. This takes place inside the run
method of the GameTimerTask
class.
EXAMPLE 8.9 shows the updated GameTimerTask
class. The only changes are at lines 17–20 of the run
method. If the bullet is off the screen (line 17), we load it inside the cannon (line 18). If it is on the screen, we test if it has been fired (line 19). If it has, we update its position by calling moveBullet
with game
at line 20. After the call to postInvalidate
at line 23, it is redrawn by the onDraw
method of the GameView
class at its new position on the screen. If the bullet is on the screen but has not been fired, that means that it is inside the cannon and there is nothing to update in this case.
FIGURE 8.3 shows the Duck Hunting Game app, Version 2, running inside the emulator, after the user moved the cannon barrel and shot a bullet.
In Version 2, when the bullet hits the duck, nothing happens. In Version 3, when the duck is hit, we play a small sound and we let the duck fall down to the ground (in fact, we let the duck go through the ground). We also make a sound when we fire a bullet.
TABLE 8.11 Selected methods of the SoundPool
class
Method | Description |
---|---|
int load( Context context, int resId, int priority ) | Loads a sound from context identified by resId as its resource id. The priority parameter is not used at the time of this writing. Use 1 as a default value. Returns the sound id. |
int play( int soundId, float leftVolume, float rightVolume, int priority, int loop, float rate ) | Plays the sound whose sound id is soundId. If loop is 0, the sound plays once, and if loop is –1, the sound plays in a loop. Rate is the playback rate, ranging from 0.5 to 2.0, and 1.0 is the normal playback rate. |
void pause( int soundId ) | Pauses the sound whose sound id is soundId. |
void resume( int soundId ) | Resumes playing the sound whose sound id is soundId. |
The SoundPool
class, part of the android.media
package, enables us to manage and play sounds. We can use its methods to:
▸ Preload a sound so that there is no delay when we play it once or in a loop.
▸ Adjust the sound volume and playback rate.
▸ Play, pause, resume a sound.
▸ Play several sounds simultaneously.
TABLE 8.11 lists some selected methods of the SoundPool
class.
The SoundPool
class contains a public static
inner class, Builder
, which we can use to create a SoundPool
object. Because that class was introduced in API level 21, we need to make sure that the minimum SDK version specified in the module gradle file is 21, as shown in EXAMPLE 8.10. We first instantiate a SoundPool.Builder
object using the default constructor of SoundPool.Builder
, then call the build
method, which returns a SoundPool
reference. TABLE 8.12 shows the SoundPool.Builder
default constructor and the build
method.
TABLE 8.12 Default constructor and the build
method of the SoundPool.Builder
class
Default Constructor | Description |
---|---|
SoundPool.Builder( ) | Constructs a SoundPool.Builder object that can play a maximum number of one stream at this point. |
Method | Description |
SoundPool build( ) | Returns a SoundPool object reference. |
SoundPool.Builder setMaxStreams ( int maxStreams ) | Sets the number of max streams that can be played at the same time. Returns this SoundPool.Builder. |
The following sequence shows how to create a SoundPool
object.
SoundPool.Builder poolBuilder = new SoundPool.Builder( ); SoundPool pool = poolBuilder.build( );
After creating a SoundPool
, the next step is to load a sound. Like we inflate a layout XML file using its id, we can load a sound using its resource id. When placing a sound resource in the res
directory, it is common to create a directory named raw
and place the sound file in it. In Version 3, we play a sound when a bullet is fired and play another sound when the duck is hit. Thus, we create the raw
directory, and add (using copy and paste) the cannon_fire.wav and duck_hit.wav sound files in it. FIGURE 8.4 shows the directory structure after having placed the cannon_fire.wav and duck_hit.wav files in the just created raw
directory.
Inside an Activity
class, we can load cannon_fire.wav using one of the load methods of SoundPool
using this statement:
// Load the first sound, located inside the raw folder, // using a Resource id // this, this Activity, ”is a” Context int fireSoundId = pool.load( this, R.raw.cannon_fire, 1 );
Note that we do not include the extension of the file name when specifying a resource in the raw
directory. That means that we should not have two sound files with the same name and different extensions in that directory.
Let’s assume that a sound named cannon_fire.wav
has been loaded and the load
method returned an integer id that we stored in the int
variable fireSoundId
. We can play the sound once (we specify 0
as the next to last argument of play
) as follows:
// Play cannon_fire.wav at regular speed once pool.play( fireSoundId, 1.0f, 1.0f, 1, 0, 1.0f );
If we have a sound named background.wav
that has been loaded and its sound id is backgroundSoundId
, we can play that sound and loop forever (we specify –1
as the next to last argument of play
) as follows:
// Play the sound at regular speed and loop forever pool.play( backgroundSoundId, 1.0f, 1.0f, 1, -1, 1.0f );
We need to play sounds whenever we fire a bullet or the duck is hit. The code to fire a bullet is in the MainActivity
class. The code to check if the duck has been hit is in the GameTimerTask
class. Thus, inside the MainActivity
class, we provide a method to play the hit sound that we can call from the GameTimerTask
class.
EXAMPLE 8.11 shows the updated MainActivity
class. At line 6, we import the SoundPool
class. At lines 18–20, we declare three instance variables: pool
, a SoundPool
reference, and fireSoundId
and hitSoundId
, two ints
storing the sound ids of the two sounds we play when a bullet is fired and when the duck is hit.
At lines 48–50, we instantiate the instance variable pool
and we enable it to play two sounds simultaneously, although we do not need that feature: both sounds are very short and since we can only shoot one bullet at a time, the fire sound and the hit sound will always be played at different times.
We load the two sounds at lines 51–52, passing to the load
method a sound resource id. R.raw.cannon_fire
and R.raw.duck_hit
identify the cannon_fire.wav and duck_hit.wav files in the raw
directory of the res
directory.
When the user double taps, we execute inside the onDoubleTapEvent
method (lines 66–72) of the TouchHandler
class. We play the bullet_fire.wav
sound (line 69) whenever we fire a bullet.
At lines 60–62, we code the playHitSound
method. Inside it, we play the duck_hit.wav
sound once. That method is not used in this class, but we call it from the GameTimerTask
class when the duck is hit.
EXAMPLE 8.12 shows the changes in the run
method of the GameTimerTask
class, the only changes in that class. We update the state of the game when the duck is shot and when the duck goes off the screen. We also play the hit sound when the duck is shot. If the duck is off the screen (line 21), not only do we want to start flying it again (line 23) but we also want to make sure that its status is that it is not shot (line 22). Indeed, it is possible that it has gone off the screen vertically after it has been shot (in which case its status was that it was shot).
If the duck is on the screen, we test if it has been hit (line 24). If it has not, there is nothing to do. If it has, we set its status to shot (line 25). That, in turn, impacts the way we move the duck inside the Game
class (vertically from top to bottom and not horizontally from right to left). We also reload the bullet (line 27) and play the hit sound to indicate that the duck has been shot. We do that at line 26 by calling the playHitSound
method of the MainActivity
class. In order to get a MainActivity
reference, we call the getContext
method with gameView
, and cast the returned Context
to MainActivity
.
We only need to make one change inside the GameView
class: display the duck properly after it has been shot. If the duck is shot, we stop animating the duck and only show one frame, the last one (the first one in this example; we could modify this example using more than four frames to animate the duck so it makes sense to show the first one). Thus, the only change in the GameView
class is the drawing of the duck, which takes place inside the onDraw
method, shown in EXAMPLE 8.13.
We test if the duck is shot at line 68. If it is, we draw the Bitmap
that corresponds to the first animation frame at lines 69–70. If the duck is not shot, we keep flying it, drawing the current Bitmap
within the array ducks
based on the value of duckFrame
at lines 72–73.
FIGURE 8.5 shows the Duck Hunting Game app, Version 3, running inside the emulator, after the user shot the duck.
The android.graphics
package includes many classes like Paint
, Canvas
, Bitmap
, BitmapFactory
that we can use to draw.
When we extend the View
class, we must override a constructor of the View
class and call the super
constructor.
To draw on a View
, we can override the onDraw
method of the View
class.
The onDraw
method has one parameter of type Canvas
. We can use it to draw shapes and bitmaps on the View
.
We can convert a file to a Bitmap
using the decodeResource
method of the BitmapFactory
class.
The various drawing methods of the Canvas
class accept a Paint
parameter. We define drawing attributes, such as color and style, by specifying the corresponding attributes of that Paint
parameter.
We can force a call to onDraw
by calling postInvalidate
with a View
reference.
We can use the Timer
class to schedule a task to be performed at a specified frequency.
To define that task, we extend the TimerTask
class and override its run
method.
The SoundPool
class can be used to manage and play sounds.
A sound can be loaded from a resource, such as a .wav sound file.
The SoundPool
class includes methods to play, pause, and resume playing a sound.
The play
method of the SoundPool
class accepts parameters to control how the sound is played (including volume and playback rate) in a loop or one or several times.
What class is used to specify what
(shapes, bitmaps, etc.) we are going to draw?
View
Paint
Canvas
Draw
What class is used to define how
(style, color, etc.) we are going to draw?
View
Paint
Canvas
Draw
If we want to draw inside a View, what method do we override?
draw
paint
onDraw
onPaint
What method of the View class can we call to force a redrawing of the View?
reDraw
rePaint
post
postInvalidate
What class has static methods that we can use to create Bitmap objects?
Bitmap
BitmapFactory
MakeBitmap
CreateBitmap
What class can we use to schedule a task to be run at a specified frequency?
System
Timer
Task
Schedule
What class should we extend to define a task that is going to be run at a specified frequency?
Task
TimerTask
TaskTimer
Scheduler
What method of the class in question 7 should be overridden to perform the task that is scheduled to run at a specified frequency?
start
task
run
thread
How many sounds can a SoundPool object play at the same time?
0
1 only
0, 1, or many
When we call the play method of the SoundPool class, what identifies the sound to be played?
The sound name
The SoundPool object
The sound id
The resource id of the sound
A Paint object named paint has been declared and instantiated. Modify it so that its color is yellow and its stroke thickness is 20.
Inside onDraw, draw a full red circle centered at (100, 200) and with radius 50.
Inside onDraw, draw an outlined green square (not green inside) centered in the middle of the current View and with side 50.
Inside onDraw, draw HELLO ANDROID in blue starting at (50, 200).
Inside onDraw, draw a bitmap somewhere on the screen from an image file named my_image.png located in the drawable directory.
Inside some class, you have an instance variable called myView that is a reference to a View. Write the code to force that View to be redrawn.
We have written a class named MyTask that extends TimerTask, including its run method. Schedule an instance of that class to run 50 times per second, starting in one second.
Inside the raw directory of the res directory, there is a sound file named my_sound.wav. Write the code to play it once.
SoundPool.Builder sb = new SoundPool.Builder( ); SoundPool pool = sb.build( ); // Your code goes here
Modify the duck hunting app in the chapter so that there are two ducks, instead of one.
Modify the duck hunting app in the chapter so that we do not have to wait until the bullet is off the screen or the duck is hit in order to shoot another bullet.
Modify the duck hunting app in the chapter to include a nice background (water, sun, grass, etc.) made up of at least one picture and three shape drawings.
Modify the duck hunting app in the chapter so that we shoot with a shotgun instead of a cannon. A shotgun shoots a spray of bullets, for example, three. You can assume that these bullets are located within a circle and move at the same speed. At least one of these bullets has to hit the duck for the duck to be shot.
Write an app that shows a pool ball (you can use any number from 1 to 15). Use only shapes, do not use bitmaps.
Write an app that is a flashlight as follows: the color can go from one color (color A) to another (color B) in continuous motion. If the screen is colored with color A, when the user swipes the screen from left to right, the screen goes from color A to color B in continuous motion. When the user swipes the screen from right to left, the screen goes from color B to color A in continuous motion.
Write an app with two activities: the first activity presents the user with a few things to draw with some style or color options, and the second activity draws them.
Write an app with two activities: the first activity displays three buttons showing web names (for example Yahoo!, Google, or Facebook). When the user clicks on one, the second activity shows a drawing of its logo.
Write a drawing app where the user can draw lines between two points by swiping the screen between these two points. Offer the user a few options for the drawing: pick among several colors, pick a thickness for the line being drawn.
Write a drawing app where the user can draw by moving his or her finger on the screen (Hint: a curve can be made of several lines). Offer the user a few options for the drawing: pick among several colors, pick a thickness for what is being drawn.
Write an app that displays a chess piece of your choice on the screen. The user can pick it up and move it to another position on the screen by touching it, moving his or her finger, and releasing it.
Write an app about the following game: Some enemy comes down vertically from the top of the screen. The player is represented by a shape, a bitmap, or even a label, at the bottom of the screen. The user can move the player by touching it and moving his or her finger. Our objective is to avoid the enemy that is coming down. When the enemy goes off the screen, another one comes down from a random location at the top of the screen. The game is over when the enemy touches the player. Include a Model.
Same as problem 30 but we add a sound when an enemy starts at the top of the screen and another sound when the enemy hits the player.
Same as problem 30 but several enemies can come down at the same time—instead of one enemy, there is an array of enemies.
Write an app simulating the game of pong. The user can move a paddle at the bottom of the screen and a ball moves on the screen. The ball bounces off the side and top edges of the screen when it hits one of them. If the ball goes past the paddle, the game is over. Include a Model.
Write a jukebox-like app, where the user can make a selection from at least five sounds and play it. Offer some options for the selected sound: one time only or in a loop, different playback rates. Include a Model.
3.14.247.9