CHAPTER EIGHT: Graphics, Animations, Sounds, and Gaming

Chapter opener image: © Fon_nongkran/Shutterstock

Introduction

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.

8.1 Graphics

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.

8.2 Making a Custom View, Drawing, Duck Hunting App, Version 0

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.

EXAMPLE 8.1 The GameView class, Duck Hunting app, Version 0

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, height100) 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.

EXAMPLE 8.2 The MainActivity class, Duck Hunting app, Version 0

FIGURE 8.1 Duck Hunting Game app, Version 0, running inside the emulator

8.3 The Model

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.

EXAMPLE 8.3 The Game class, Duck Hunting app, Version 1

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.

8.4 Animating an Object: Flying the Duck, Duck Hunting App, Version 1

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 The GameTimerTask class, Duck Hunting app, Version 1

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.

EXAMPLE 8.5 The GameView class, Duck Hunting app, Version 1

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.

EXAMPLE 8.6 Edits in the MainActivity class, Duck Hunting app, Version 1

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.

FIGURE 8.2 Duck Hunting Game app, Version 1, running inside the emulator

8.5 Handling Touch Events: Moving the Cannon and Shooting, Duck Hunting App, Version 2

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).

EXAMPLE 8.7 The MainActivity class, Duck Hunting app, Version 2

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.

EXAMPLE 8.8 The onDraw Method of the GameView class, Duck Hunting app Version 2

EXAMPLE 8.9 The GameTimerTask class, Duck Hunting app, Version 2

FIGURE 8.3 Duck Hunting app, Version 2, running inside the emulator

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.

8.6 Playing a Sound: Shooting, Collision Detection, Duck Hunting App, Version 3

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.

EXAMPLE 8.10 Parts of the build.gradle (Module:app) file with a minimum SDK of 21

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 );

FIGURE 8.4 The cannon_fire.wav and duck_hit.wav files in the raw directory

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.

EXAMPLE 8.11 The MainActivity class, Duck Hunting app, Version 3

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.

EXAMPLE 8.12 The run method of the GameTimerTask class, Duck Hunting app, Version 3

EXAMPLE 8.13 The updated onDraw method of the GameView class, Duck Hunting app, Version 3

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.

FIGURE 8.5 Duck Hunting Game app, Version 3, running inside the emulator, after the user shot the duck

Chapter Summary

  • 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.

Exercises, Problems, and Projects

Multiple-Choice Exercises

  1. What class is used to specify what (shapes, bitmaps, etc.) we are going to draw?

    • View

    • Paint

    • Canvas

    • Draw

  2. What class is used to define how (style, color, etc.) we are going to draw?

    • View

    • Paint

    • Canvas

    • Draw

  3. If we want to draw inside a View, what method do we override?

    • draw

    • paint

    • onDraw

    • onPaint

  4. What method of the View class can we call to force a redrawing of the View?

    • reDraw

    • rePaint

    • post

    • postInvalidate

  5. What class has static methods that we can use to create Bitmap objects?

    • Bitmap

    • BitmapFactory

    • MakeBitmap

    • CreateBitmap

  6. What class can we use to schedule a task to be run at a specified frequency?

    • System

    • Timer

    • Task

    • Schedule

  7. What class should we extend to define a task that is going to be run at a specified frequency?

    • Task

    • TimerTask

    • TaskTimer

    • Scheduler

  8. 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

  9. How many sounds can a SoundPool object play at the same time?

    • 0

    • 1 only

    • 0, 1, or many

  10. 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

Fill in the Code

  1. A Paint object named paint has been declared and instantiated. Modify it so that its color is yellow and its stroke thickness is 20.

  2. Inside onDraw, draw a full red circle centered at (100, 200) and with radius 50.

  3. Inside onDraw, draw an outlined green square (not green inside) centered in the middle of the current View and with side 50.

  4. Inside onDraw, draw HELLO ANDROID in blue starting at (50, 200).

  5. Inside onDraw, draw a bitmap somewhere on the screen from an image file named my_image.png located in the drawable directory.

  6. 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.

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

  8. 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
    

Write an app

  1. Modify the duck hunting app in the chapter so that there are two ducks, instead of one.

  2. 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.

  3. 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.

  4. 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.

  5. Write an app that shows a pool ball (you can use any number from 1 to 15). Use only shapes, do not use bitmaps.

  6. 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.

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

  8. 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.

  9. 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.

  10. 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.

  11. 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.

  12. 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.

  13. 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.

  14. Same as problem 30 but several enemies can come down at the same time—instead of one enemy, there is an array of enemies.

  15. 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.

  16. 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.

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

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