Objectives
In this chapter you’ll:
• Detect when the user touches the screen, moves a finger across the screen and removes a finger from the screen.
• Process multiple screen touches so the user can draw with multiple fingers at once.
• Use a SensorManager
to detect accelerometer motion events to clear the screen when the user shakes the device.
• Use an AtomicBoolean
object to allow multiple threads to access a boolean
value in a thread-safe manner.
• Use a Paint
object to specify the color and width of a line.
• Use Path
objects to store each line’s data as the user draws the lines and to draw those lines with a Canvas
.
• Use a Toast
to briefly display a message on the screen.
9.2 Test-Driving the Doodlz App
9.4 Building the App’s GUI and Resource Files
9.5.1 Doodlz
Subclass of Activity
9.5.2 DoodleView
Subclass of View
Self-Review Exercises | Answers to Self-Review Exercises | Exercises
The Doodlz app turns your device’s screen into a virtual canvas (Fig. 9.1). You paint by dragging one or more fingers across the screen. The app’s options enable you to set the drawing color and line width. The Choose Color dialog (Fig. 9.2(a)) provides alpha (transparency), red, green and blue SeekBar
s (i.e., sliders) that allow you to select the ARGB color. As you move the thumb on each SeekBar
, the color swatch below the SeekBar
s shows you the current color. The Choose Line Width dialog (Fig. 9.2(b)) provides a single SeekBar
that controls the thickness of the line that you’ll draw. Additional menu items (Fig. 9.3) allow you to turn your finger into an eraser (Erase), to clear the screen (Clear) and to save the current drawing into your device’s Gallery (Save Image). At any point, you can shake the device to clear the entire drawing from the screen.
You test drove this app in Section 1.11, so we do not present a test drive in this chapter.
This section presents the many new technologies that we use in the Doodlz app in the order they’re encountered throughout the chapter.
Though we don’t use any Android-3.0 features in this app, we specify in the app’s manifest that we target the Android 3.0 SDK (Section 9.4.2). Doing so allows the app’s GUI components to use Android 3.0’s look-and-feel—the so-called holographic theme—on Android tablet devices. In addition, the app’s menu is displayed at the right side of the Android 3.0 action bar, which appears at the top of the screen on tablet devices.
SensorManager
to Listen for Accelerometer EventsThis app allows the user to shake the device to erase the current drawing. Most devices have an accelerometer that allows apps to detect movement. Other sensors currently supported by Android include gravity, gyroscope, light, linear acceleration, magnetic field, pressure, proximity, rotation vector and temperature. The list of Sensor
constants representing the sensor types can be found at:
developer.android.com/reference/android/hardware/Sensor.html
To listen for sensor events, you get a reference to the system’s SensorManager
service (Section 9.5.1), which enables the app to receive data from the device’s sensors. You use the SensorManager
to register the sensor changes that your app should receive and to specify the SensorEventListener
that will handle those sensor-change events. The classes and interfaces for processing sensor events are located in package android.hardware
.
Dialog
sSeveral previous apps have used AlertDialog
s to display information to the user or to ask questions and receive responses from the user in the form of Button
clicks. AlertDialog
s can display only simple String
s and Button
s. For more complex dialogs, you can use objects of class Dialog
(package android.app
) that display custom GUIs (Section 9.5.1). In this app, we use these to allow the user to select a drawing color or select a line width, and we inflate each Dialog
’s GUI from an XML layout file (Figs. 9.7–Fig. 9.8).
AtomicBoolean
In Android, sensor events are handled in a separate thread of execution from GUI events. Therefore, it’s possible that the event handler for the shake event could try to display the confirmation dialog for erasing an image when another dialog is already on the screen. To prevent this, we’ll use an AtomicBoolean
(package import java.util.concurrent.atomic
) to indicate when a dialog is currently displayed. An AtomicBoolean
manages a boolean
value in a thread-safe manner, so that it can be accessed from multiple threads of execution. When the AtomicBoolean
’s value is true
, we will not allow the event handler for the shake event to display a dialog.
Color
sThe user can set a custom drawing Color
(Section 9.5.1) in this app by specifying the alpha, red, green and blue components of the Color
with SeekBar
s in a Dialog
. Each value is in the range 0 to 255. The alpha component specifies the Color
’s transparency with 0 representing completely transparent and 255 representing completely opaque. Class Color
provides methods for assembling a Color
from its component values (which we need to set the custom drawing Color
) and for obtaining the component values from a Color
(which we need to set the initial values of the SeekBar
s in the Choose Color dialog).
Path
sThis app draws lines onto Bitmap
s (package android.graphics
). You can associate a Canvas
with a Bitmap
, then use the Canvas
to draw on the Bitmap
, which can then be displayed on the screen (Sections 9.5.1– and 9.5.2). A Bitmap
can also be saved into a file—we’ll use this capability to store drawings in the device’s gallery when the user touches the Save Image menu item.
The user can touch the screen with one or more fingers and drag the fingers to draw lines. We store the information for each individual finger as a Path
object (package android.graphics
), which represents a geometric path consisting of line segments and curves. Touch events are processed by overriding the View
method OnTouchEvent
(Section 9.5.2). This method receives a MotionEvent
(package android.view
) that contains the type of touch event that occurred and the ID of the finger (i.e., pointer) that generated the event. We use the IDs to distinguish the different fingers and add information to the corresponding Path
objects. We use the type of the touch event to determine whether the user has touched the screen, dragged across the screen or lifted a finger from the screen.
The app provides a Save Image menu item that allows the user to save a drawing into the device’s gallery—the default location in which photos taken with the device are stored. A ContentResolver
(package android.content
) enables the app to read data from and store data on a device. We’ll use one (Section 9.5.2) to get an OutputStream
for writing data into the gallery and save the image in JPEG format.
Toast
s to Display a Message for a Short TimeA Toast
(package android.widget
) displays a message for a short time, then disappears from the screen. These are often used to display minor error messages or informational messages, such as an indication that an app’s data has been refreshed. We use one (Section 9.5.2) to indicate whether or not the user’s drawing was successfully saved to the gallery.
In this section, you’ll create the Doodlz app’s resource files and GUI layout files.
Begin by creating a new Android project named Doodlz
. Specify the following values in the New Android Project dialog, then press Finish:
• Build Target: Ensure that Android 2.3.3 is checked
• Application name: Doodlz
• Package name: com.deitel.doodlz
• Min SDK Version: 8
AndroidManifest.xml
Figure 9.4 shows this app’s AndroidManifest.xml
file. In this app, we set the uses-sdk
element’s android:targetSdkVersion
attribute to "11"
(line 15), which represents the Android 3.0 SDK. If this app is installed on a device running Android 3.0 or higher, Android 3.0’s holographic theme will be applied to the app’s GUI components, and the menu items will be placed at the right side of the app’s action bar, which appears at the top of the screen on tablet devices. Setting the android:targetSdkVersion
attribute to "11"
has no effect when the app is installed on a device running an earlier version of Android. Targeting SDK version 11 is recommended for any apps that you’d like users to install on Android tablets, so the apps have the look-and-feel of those that are developed specifically for Android 3.0 and higher.
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 android:versionCode="1" android:versionName="1.0"
4 package="com.deitel.doodlz">
5 <application android:icon="@drawable/icon"
6 android:label="@string/app_name" android:debuggable="true">
7 <activity android:label="@string/app_name" android:name=".Doodlz"
8 android:screenOrientation="portrait">
9 <intent-filter>
10 <action android:name="android.intent.action.MAIN" />
11 <category android:name="android.intent.category.LAUNCHER"/>
12 </intent-filter>
13 </activity>
14 </application>
15 <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/>
16 </manifest>
strings.xml
Figure 9.5 defines the String
resources used in this app.
1 <?xml version="1.0" encoding="utf-8"?>
2 <resources>
3 <string name="app_name">Doodlz</string>
4 <string name="button_erase">Erase</string>
5 <string name="button_cancel">Cancel</string>
6 <string name="button_set_color">Set Color</string>
7 <string name="button_set_line_width">Set Line Width</string>
8 <string name="label_alpha">Alpha</string>
9 <string name="label_red">Red</string>
10 <string name="label_green">Green</string>
11 <string name="label_blue">Blue</string>
12 <string name="menuitem_clear">Clear</string>
13 <string name="menuitem_color">Color</string>
14 <string name="menuitem_erase">Erase</string>
15 <string name="menuitem_line_width">Line Width</string>
16 <string name="menuitem_save_image">Save Image</string>
17 <string name="message_erase">Erase the drawing?</string>
18 <string name="message_error_saving">
19 There was an error saving the image</string>
20 <string name="message_saved">
21 Your painting has been saved to the Gallery</string>
22 <string name="title_color_dialog">Choose Color</string>
23 <string name="title_line_width_dialog">Choose Line Width</string>
24 </resources>
main.xml
We deleted the default main.xml
file and replaced it with a new one. In this case, the only component in the layout is an instance of our custom View
subclass, DoodleView
, which you’ll add to the project in Section 9.5.2. Figure 9.6 shows the completed main.xml
in which we manually entered the XML element shown in lines 2–5—our custom DoodleView
is not in the ADT’s Palette, so it cannot be dragged and dropped onto the layout.
1 <?xml version="1.0" encoding="utf-8"?>
2 <com.deitel.doodlz.DoodleView "
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"/>
color_dialog.xml
Figure 9.7 shows the completed color_dialog.xml
, which defines the GUI for a dialog that allows the user to specify the alpha, red, green and blue components of the drawing color. The LinearLayout
(lines 61–67) has a white background and contains a View
(lines 64–66) that we use to display the current drawing color based on the values of the four SeekBar
s, each allowing the user to select values from 0 (the default minimum) to 255 (the specified maximum). The white background enables the color to display accurately on the View
when the user makes the color semitransparent with the alphaSeekBar
. We use the standard SeekBar
thumb in our apps, but you can customize it by setting the SeekBar
’s android:thumb
attribute to a drawable resource, such as an image.
1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/colorDialogLinearLayout"
4 android:layout_width="match_parent" android:minWidth="300dp"
5 android:layout_height="match_parent" android:orientation="vertical">
6
7 <TableLayout android:id="@+id/tableLayout"
8 android:layout_width="match_parent"
9 android:layout_height="wrap_content" android:layout_margin="10dp"
10 android:stretchColumns="1">
11 <TableRow android:orientation="horizontal"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content">
14 <TextView android:layout_width="wrap_content"
15 android:layout_height="wrap_content"
16 android:text="@string/label_alpha" android:gravity="right"
17 android:layout_gravity="center_vertical"/>
18 <SeekBar android:id="@+id/alphaSeekBar"
19 android:layout_width="wrap_content"
20 android:layout_height="wrap_content" android:max="255"
21 android:paddingLeft="10dp" android:paddingRight="10dp"/>
22 </TableRow>
23 <TableRow android:orientation="horizontal"
24 android:layout_width="match_parent"
25 android:layout_height="wrap_content">
26 <TextView android:layout_width="wrap_content"
27 android:layout_height="wrap_content"
28 android:text="@string/label_red" android:gravity="right"
29 android:layout_gravity="center_vertical"/>
30 <SeekBar android:id="@+id/redSeekBar"
31 android:layout_width="wrap_content"
32 android:layout_height="wrap_content" android:max="255"
33 android:paddingLeft="10dp" android:paddingRight="10dp"/>
34 </TableRow>
35 <TableRow android:orientation="horizontal"
36 android:layout_width="match_parent"
37 android:layout_height="wrap_content">
38 <TextView android:layout_width="wrap_content"
39 android:layout_height="wrap_content"
40 android:text="@string/label_green" android:gravity="right"
41 android:layout_gravity="center_vertical"/>
42 <SeekBar android:id="@+id/greenSeekBar"
43 android:layout_width="wrap_content"
44 android:layout_height="wrap_content" android:max="255"
45 android:paddingLeft="10dp" android:paddingRight="10dp"/>
46 </TableRow>
47 <TableRow android:orientation="horizontal"
48 android:layout_width="wrap_content"
49 android:layout_height="wrap_content">
50 <TextView android:layout_width="match_parent"
51 android:layout_height="wrap_content"
52 android:text="@string/label_blue" android:gravity="right"
53 android:layout_gravity="center_vertical"/>
54 <SeekBar android:id="@+id/blueSeekBar"
55 android:layout_width="wrap_content"
56 android:layout_height="wrap_content" android:max="255"
57 android:paddingLeft="10dp" android:paddingRight="10dp"/>
58 </TableRow>
59 </TableLayout>
60
61 <LinearLayout android:background="@android:color/white"
62 android:layout_width="match_parent"
63 android:layout_height="wrap_content" android:layout_margin="10dp">
64 <View android:id="@+id/colorView"
65 android:layout_width="match_parent"
66 android:layout_height="30dp"/>
67 </LinearLayout>
68
69 <Button android:id="@+id/setColorButton"
70 android:layout_width="wrap_content"
71 android:layout_height="wrap_content"
72 android:layout_gravity="center_horizontal"
73 android:text="@string/button_set_color"/>
74 </LinearLayout>
width_dialog.xml
Figure 9.8 shows the completed width_dialog.xml
, which defines the GUI for a dialog that allows the user to specify the line width for drawing. As the user moves the widthSeekBar
’s thumb, we use the ImageView
(lines 6–8) to display a sample line in the current line width and current color.
1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/widthDialogLinearLayout"
4 android:layout_width="match_parent" android:minWidth="300dp"
5 android:layout_height="match_parent" android:orientation="vertical">
6 <ImageView android:id="@+id/widthImageView"
7 android:layout_width="match_parent" android:layout_height="50dp"
8 android:layout_margin="10dp"/>
9 <SeekBar android:layout_height="wrap_content" android:max="50"
10 android:id="@+id/widthSeekBar" android:layout_width="match_parent"
11 android:layout_margin="20dp" android:paddingLeft="20dp"
12 android:paddingRight="20dp"
13 android:layout_gravity="center_horizontal"/>
14 <Button android:id="@+id/widthDialogDoneButton"
15 android:layout_width="wrap_content"
16 android:layout_height="wrap_content"
17 android:layout_gravity="center_horizontal"
18 android:text="@string/button_set_line_width"/>
19 </LinearLayout>
This app consists of two classes—class Doodlz
(the Activity
subclass; Figs. 9.9–9.20) and class DoodleView
(Figs. 9.21–9.29).
1 // Doodlz.java
2 // Draws View which changes color in response to user touches.
3 package com.deitel.doodlz;
4
5 import java.util.concurrent.atomic.AtomicBoolean;
6
7 import android.app.Activity;
8 import android.app.AlertDialog;
9 import android.app.Dialog;
10 import android.content.Context;
11 import android.content.DialogInterface;
12 import android.graphics.Bitmap;
13 import android.graphics.Canvas;
14 import android.graphics.Color;
15 import android.graphics.Paint;
16 import android.hardware.Sensor;
17 import android.hardware.SensorEvent;
18 import android.hardware.SensorEventListener;
19 import android.hardware.SensorManager;
20 import android.os.Bundle;
21 import android.view.Menu;
22 import android.view.MenuItem;
23 import android.view.View;
24 import android.view.View.OnClickListener;
25 import android.widget.Button;
26 import android.widget.ImageView;
27 import android.widget.SeekBar;
28 import android.widget.SeekBar.OnSeekBarChangeListener;
29
Doodlz
Subclass of Activity
Class Doodlz
(Figs. 9.9–9.20) is the Doodlz app’s main Activity
. It provides the app’s menu, dialogs and accelerometer event handling.
package
and import
StatementsSection 9.3 discussed the key new classes and interfaces that class Doodlz
uses. We’ve highlighted these classes and interfaces in Fig. 9.9.
Figure 9.10 shows the instance variables and constants of class Doodlz
. DoodleView
variable doodleView
(line 32) represents the drawing area. The sensorManager
is used to monitor the accelerometer to detect the device movement. The float
variables declared in lines 34–36 are used to calculate changes in the device’s acceleration to determine when a shake event occurs (so we can ask whether the user would like to erase the drawing), and the constant in line 47 is used to ensure that small movements are not interpreted as shakes—we picked this constant via trial and error by shaking the app on several devices. Line 37 defines the AtomicBoolean
object (with the value false
by default) that will be used throughout this class to specify when there is a dialog displayed on the screen, so we can prevent multiple dialogs from being displaed at the same time. Lines 40–44 declare the int
constants for the app’s five menu items. We use the Dialog
variable currentDialog
(line 50) to refer to the Choose Color or Choose Line Width dialogs that allow the user to change the drawing color and line width, respectively.
30 public class Doodlz extends Activity
31 {
32 private DoodleView doodleView; // drawing View
33 private SensorManager sensorManager; // monitors accelerometer
34 private float acceleration; // acceleration
35 private float currentAcceleration; // current acceleration
36 private float lastAcceleration; // last acceleration
37 private AtomicBoolean dialogIsDisplayed = new AtomicBoolean(); // false
38
39 // create menu ids for each menu option
40 private static final int COLOR_MENU_ID = Menu.FIRST;
41 private static final int WIDTH_MENU_ID = Menu.FIRST + 1;
42 private static final int ERASE_MENU_ID = Menu.FIRST + 2;
43 private static final int CLEAR_MENU_ID = Menu.FIRST + 3;
44 private static final int SAVE_MENU_ID = Menu.FIRST + 4;
45
46 // value used to determine whether user shook the device to erase
47 private static final int ACCELERATION_THRESHOLD = 15000;
48
49 // variable that refers to a Choose Color or Choose Line Width dialog
50 private Dialog currentDialog;
51
Activity
Methods onCreate
and onPause
Class Doodlz
’s onCreate
method (Fig. 9.11) gets a reference to the DoodleView
, then initializes the instance variables that help calculate acceleration changes to determine whether the user shook the device to erase the drawing. We initially set variables currentAcceleration
and lastAcceleration
to SensorManager
’s GRAVITY_EARTH
constant, which represents the acceleration due to gravity on earth. SensorManager
also provides constants for other planets in the solar system, for the moon and for several other entertaining values, which you can see at:
developer.android.com/reference/android/hardware/SensorManager.html
52 // called when this Activity is loaded
53 @Override
54 protected void onCreate(Bundle savedInstanceState)
55 {
56 super.onCreate(savedInstanceState);
57 setContentView(R.layout.main); // inflate the layout
58
59 // get reference to the DoodleView
60 doodleView = (DoodleView) findViewById(R.id.doodleView);
61
62 // initialize acceleration values
63 acceleration = 0.00f;
64 currentAcceleration = SensorManager.GRAVITY_EARTH;
65 lastAcceleration = SensorManager.GRAVITY_EARTH;
66
67 enableAccelerometerListening(); // listen for shake
68 } // end method onCreate
69
70 // when app is sent to the background, stop listening for sensor events
71 @Override
72 protected void onPause()
73 {
74 super.onPause();
75 disableAccelerometerListening(); // don't listen for shake
76 } // end method onPause
77
Next, line 67 calls method enableAccelerometerListening
(Fig. 9.12) to configure the SensorManager
to listen for accelerometer events. Class Doodlz
’s onPause
method (lines 71–76) calls method disableAccelerometerListening
(Fig. 9.12) to unregister the accelerometer event handler when the app is sent to the background.
78 // enable listening for accelerometer events
79 private void enableAccelerometerListening()
80 {
81 // initialize the SensorManager
82 sensorManager =
83 (SensorManager) getSystemService(Context.SENSOR_SERVICE);
84 sensorManager.registerListener(sensorEventListener,
85 sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
86 SensorManager.SENSOR_DELAY_NORMAL);
87 } // end method enableAccelerometerListening
88
89 // disable listening for accelerometer events
90 private void disableAccelerometerListening()
91 {
92 // stop listening for sensor events
93 if (sensorManager != null)
94 {
95 sensorManager.unregisterListener(
96 sensorEventListener,
97 sensorManager.getDefaultSensor(
98 SensorManager.SENSOR_ACCELEROMETER));
99 sensorManager = null;
100 } // end if
101 } // end method disableAccelerometerListening
102
enableAccelerometerListening
and disableAccelerometerListening
Method enableAccelerometerListening
(Fig. 9.12; lines 79–87) configures the SensorManager
. Lines 82–83 use Activity
’s getSystemService
method to retrieve the system’s SensorManager
service, which enables the app to interact with the device’s sensors. We then register to receive accelerometer events using SensorManager
’s registerListener
method, which receives three arguments:
• the SensorEventListener
object that will respond to the events
• a Sensor
representing the type of sensor data the app wishes to receive. This is retrieved by calling SensorManager
’s getDefaultSensor
method and passing a Sensor
-type constant (Sensor.TYPE_ACCELEROMETER
in this app).
• a rate at which sensor events should be delivered to the app. We chose SENSOR_DELAY_NORMAL
to receive sensor events at the default rate—a faster rate can be used to get more accurate data, but this is also more resource intensive.
Method disableAccelerometerListening
(Fig. 9.12; lines 90–101), which is called from onPause
, uses class SensorManager
’s unregisterListener
method to stop listening for accelerometer events. Since we don’t know whether the app will return to the foreground, we also set the sensorManager
reference to null
.
SensorEventListener
to Process Accelerometer EventsFigure 9.13 overrides SensorEventListener
method onSensorChanged
(lines 108–168) to process accelerometer events. If the user moves the device, this method attempts to determine whether the movement was enough to be considered a shake. If so, lines 133–165 build and display an AlertDialog
asking the user whether the drawing should be erased. Interface SensorEventListener
also contains method onAccuracyChanged
(lines 171–174)—we don’t use this method in this app, so we provide an empty body.
103 // event handler for accelerometer events
104 private SensorEventListener sensorEventListener =
105 new SensorEventListener()
106 {
107 // use accelerometer to determine whether user shook device
108 @Override
109 public void onSensorChanged(SensorEvent event)
110 {
111 // ensure that other dialogs are not displayed
112 if (!dialogIsVisible.get())
113 {
114 // get x, y, and z values for the SensorEvent
115 float x = event.values[0];
116 float y = event.values[1];
117 float z = event.values[2];
118
119 // save previous acceleration value
120 lastAcceleration = currentAcceleration;
121
122 // calculate the current acceleration
123 currentAcceleration = x * x + y * y + z * z;
124
125 // calculate the change in acceleration
126 acceleration = currentAcceleration *
127 (currentAcceleration - lastAcceleration);
128
129 // if the acceleration is above a certain threshold
130 if (acceleration > ACCELERATION_THRESHOLD)
131 {
132 // create a new AlertDialog Builder
133 AlertDialog.Builder builder =
134 new AlertDialog.Builder(Doodlz.this);
135
136 // set the AlertDialog's message
137 builder.setMessage(R.string.message_erase);
138 builder.setCancelable(true);
139
140 // add Erase Button
141 builder.setPositiveButton(R.string.button_erase,
142 new DialogInterface.OnClickListener()
143 {
144 public void onClick(DialogInterface dialog, int id)
145 {
146 dialogIsVisible.set(false);
147 doodleView.clear(); // clear the screen
148 } // end method onClick
149 } // end anonymous inner class
150 ); // end call to setPositiveButton
151
152 // add Cancel Button
153 builder.setNegativeButton(R.string.button_cancel,
154 new DialogInterface.OnClickListener()
155 {
156 public void onClick(DialogInterface dialog, int id)
157 {
158 dialogIsVisible.set(false);
159 dialog.cancel(); // dismiss the dialog
160 } // end method onClick
161 } // end anonymous inner class
162 ); // end call to setNegativeButton
163
164 dialogIsVisible.set(true); // dialog is on the screen
165 builder.show(); // display the dialog
166 } // end if
167 } // end if
168 } // end method onSensorChanged
169
170 // required method of interface SensorEventListener
171 @Override
172 public void onAccuracyChanged(Sensor sensor, int accuracy)
173 {
174 } // end method onAccuracyChanged
175 }; // end anonymous inner class
176
The user can shake the device even when dialogs are already displayed on the screen. For this reason, onSensorChanged
first checks whether a dialog is displayed by calling dialogIsVisible
’s get
method (line 110). This test ensures that no other dialogs are displayed. This is important because the sensor events occur in a different thread of execution. Without this test, we’d be able to display the confirmation dialog for erasing the image when another dialog is on the screen.
The SensorEvent
parameter contains information about the sensor change that occurred. For accelerometer events, this parameter’s values
array contains three elements representing the acceleration (in meter/second2) in the x (left/right), y (up/down) and z (forward/backward) directions. A description and diagram of the coordinate system used by the SensorEvent
API is available at:
developer.android.com/reference/android/hardware/SensorEvent.html
This link also describes the real-world meanings for a SensorEvent
’s x, y and z values for each different Sensor
.
We store the acceleration values (lines 115–117), then store the last value of currentAcceleration
(line 120). Line 123 sums the squares of the x
, y
and z
acceleration values and stores them in currentAcceleration
. Then, using the currentAcceleration
and lastAcceleration
values, we calculate a value (acceleration
) that can be compared to our ACCELERATION_THRESHOLD
constant. If the value is greater than the constant, the user moved the device enough for this app to consider the movement a shake. In this case, we set shakeDetected
to true
, then configure and display an AlertDialog
in which the user can confirm that the shake should erase the drawing or cancel the dialog. Setting variable shakeDetected
to true
ensures that while the confirmation dialog is displayed, method onSensorChanged
will not display another dialog if the user shakes the device again. If the user confirms that the drawing should be erased, line 147 calls the DoodleView
’s clear
method (Fig. 9.23). [Note: It’s important to handle sensor events quickly or to copy the event data (as we did) because the array of sensor values is reused for each sensor event.]
onCreateOptionsMenu
and onOptionsItemSelected
Figure 9.14 overrides Activity
’s onCreateOptionsMenu
method to setup the Activity
’s menu. We use the menu
’s add
method to add menu items (lines 184–193). Recall that the first argument is the group identifier, which can be used to group items together. We do not have any groups, so we use Menu
’s NONE
constant for each item. The second argument is the item’s unique identifier—one of the constants declared in lines 40–44. The third argument specifies the menu item’s order with respect to the other menu items. We use Menu
’s NONE
constant, because the order is not important in this app. This value allows the item’s sizes to determine how Android lays out the menu items. The final argument is the String
resource to display on each menu item.
177 // displays configuration options in menu
178 @Override
179 public boolean onCreateOptionsMenu(Menu menu)
180 {
181 super.onCreateOptionsMenu(menu); // call super's method
182
183 // add options to menu
184 menu.add(Menu.NONE, COLOR_MENU_ID, Menu.NONE,
185 R.string.menuitem_color);
186 menu.add(Menu.NONE, WIDTH_MENU_ID, Menu.NONE,
187 R.string.menuitem_line_width);
188 menu.add(Menu.NONE, ERASE_MENU_ID, Menu.NONE,
189 R.string.menuitem_erase);
190 menu.add(Menu.NONE, CLEAR_MENU_ID, Menu.NONE,
191 R.string.menuitem_clear);
192 menu.add(Menu.NONE, SAVE_MENU_ID, Menu.NONE,
193 R.string.menuitem_save_image);
194
195 return true; // options menu creation was handled
196 } // end onCreateOptionsMenu
197
198 // handle choice from options menu
199 @Override
200 public boolean onOptionsItemSelected(MenuItem item)
201 {
202 // switch based on the MenuItem id
203 switch (item.getItemId())
204 {
205 case COLOR_MENU_ID:
206 showColorDialog(); // display color selection dialog
207 return true; // consume the menu event
208 case WIDTH_MENU_ID:
209 showLineWidthDialog(); // display line thickness dialog
210 return true; // consume the menu event
211 case ERASE_MENU_ID:
212 doodleView.setDrawingColor(Color.WHITE); // line color white
213 return true; // consume the menu event
214 case CLEAR_MENU_ID:
215 doodleView.clear(); // clear doodleView
216 return true; // consume the menu event
217 case SAVE_MENU_ID:
218 doodleView.saveImage(); // save the current images
219 return true; // consume the menu event
220 } // end switch
221
222 return super.onOptionsItemSelected(item); // call super's method
223 } // end method onOptionsItemSelected
224
Lines 199–223 override Activity
’s onOptionItemSelected
method, which is called when the user touches a menu item. We use the MenuItem
argument’s ID (line 203) to take different actions depending on the item the user selected. The actions are as follows:
• For Color, line 206 calls method showColorDialog
(Fig. 9.15) to allow the user to select a new drawing color.
225 // display a dialog for selecting color
226 private void showColorDialog()
227 {
228 // create the dialog and inflate its content
229 currentDialog = new Dialog(this);
230 currentDialog.setContentView(R.layout.color_dialog);
231 currentDialog.setTitle(R.string.title_color_dialog);
232 currentDialog.setCancelable(true);
233
234 // get the color SeekBars and set their onChange listeners
235 final SeekBar alphaSeekBar =
236 (SeekBar) currentDialog.findViewById(R.id.alphaSeekBar);
237 final SeekBar redSeekBar =
238 (SeekBar) currentDialog.findViewById(R.id.redSeekBar);
239 final SeekBar greenSeekBar =
240 (SeekBar) currentDialog.findViewById(R.id.greenSeekBar);
241 final SeekBar blueSeekBar =
242 (SeekBar) currentDialog.findViewById(R.id.blueSeekBar);
243
244 // register SeekBar event listeners
245 alphaSeekBar.setOnSeekBarChangeListener(colorSeekBarChanged);
246 redSeekBar.setOnSeekBarChangeListener(colorSeekBarChanged);
247 greenSeekBar.setOnSeekBarChangeListener(colorSeekBarChanged);
248 blueSeekBar.setOnSeekBarChangeListener(colorSeekBarChanged);
249
250 // use current drawing color to set SeekBar values
251 final int color = doodleView.getDrawingColor();
252 alphaSeekBar.setProgress(Color.alpha(color));
253 redSeekBar.setProgress(Color.red(color));
254 greenSeekBar.setProgress(Color.green(color));
255 blueSeekBar.setProgress(Color.blue(color));
256
257 // set the Set Color Button's onClickListener
258 Button setColorButton = (Button) currentDialog.findViewById(
259 R.id.setColorButton);
260 setColorButton.setOnClickListener(setColorButtonListener);
261
262 dialogIsVisible.set(true); // dialog is on the screen
263 currentDialog.show(); // show the dialog
264 } // end method showColorDialog
265
• For Width, line 209 calls method showLineWidthDialog
(Fig. 9.18) to allow the uset to select a new line width.
• For Erase, line 212 sets the doodleView
’s drawing color to white, which effectively turns the user’s fingers into erasers.
• For Clear, line 215 calls the doodleView
’s clear
method to remove all painted lines from the display.
• For Save, line 218 calls doodleView
’s saveImage
method to save the painting as an image stored in the device’s image gallery.
showColorDialog
The showColorDialog
method (Fig. 9.15) creates a Dialog
and sets its GUI by calling setContentView
to inflate color_dialog.xml
(lines 229–230). We also set the dialog’s title and indicate that it’s cancelable—the user can press the device’s back button to dismiss the dialog without making any changes to the current color. Lines 235–242 get references to the dialog’s four SeekBar
s, then lines 256–248 set each SeekBar
’s OnSeekBarChangeListener
to the colorSeekBarChanged
listener (Fig. 9.16). Lines 251–255 get the current drawing color from doodleView
, then use it to set each SeekBar
’s current value. Color
’s static
methods alpha
, red
, green
and blue
are used to extract the ARGB values from the current color, and SeekBar
’s setProgress
method positions the thumbs. Lines 258–260 get a reference to the dialog’s setColorButton
and register setColorButtonListener
(Fig. 9.17) as its event handler. Line 262 indicates that a dialog is displayed by calling isDialigVisible
’s set
method with the value true
. Finally, line 263 displays the Dialog
using its show
method. The new color is set only if the user touches the Set Color Button
in the Dialog
.
266 // OnSeekBarChangeListener for the SeekBars in the color dialog
267 private OnSeekBarChangeListener colorSeekBarChanged =
268 new OnSeekBarChangeListener()
269 {
270 @Override
271 public void onProgressChanged(SeekBar seekBar, int progress,
272 boolean fromUser)
273 {
274 // get the SeekBars and the colorView LinearLayout
275 SeekBar alphaSeekBar =
276 (SeekBar) currentDialog.findViewById(R.id.alphaSeekBar);
277 SeekBar redSeekBar =
278 (SeekBar) currentDialog.findViewById(R.id.redSeekBar);
279 SeekBar greenSeekBar =
280 (SeekBar) currentDialog.findViewById(R.id.greenSeekBar);
281 SeekBar blueSeekBar =
282 (SeekBar) currentDialog.findViewById(R.id.blueSeekBar);
283 View colorView =
284 (View) currentDialog.findViewById(R.id.colorView);
285
286 // display the current color
287 colorView.setBackgroundColor(Color.argb(
288 alphaSeekBar.getProgress(), redSeekBar.getProgress(),
289 greenSeekBar.getProgress(), blueSeekBar.getProgress()));
290 } // end method onProgressChanged
291
292 // required method of interface OnSeekBarChangeListener
293 @Override
294 public void onStartTrackingTouch(SeekBar seekBar)
295 {
296 } // end method onStartTrackingTouch
297
298 // required method of interface OnSeekBarChangeListener
299 @Override
300 public void onStopTrackingTouch(SeekBar seekBar)
301 {
302 } // end method onStopTrackingTouch
303 }; // end colorSeekBarChanged
304
305 // OnClickListener for the color dialog's Set Color Button
306 private OnClickListener setColorButtonListener = new OnClickListener()
307 {
308 @Override
309 public void onClick(View v)
310 {
311 // get the color SeekBars
312 SeekBar alphaSeekBar =
313 (SeekBar) currentDialog.findViewById(R.id.alphaSeekBar);
314 SeekBar redSeekBar =
315 (SeekBar) currentDialog.findViewById(R.id.redSeekBar);
316 SeekBar greenSeekBar =
317 (SeekBar) currentDialog.findViewById(R.id.greenSeekBar);
318 SeekBar blueSeekBar =
319 (SeekBar) currentDialog.findViewById(R.id.blueSeekBar);
320
321 // set the line color
322 doodleView.setDrawingColor(Color.argb(
323 alphaSeekBar.getProgress(), redSeekBar.getProgress(),
324 greenSeekBar.getProgress(), blueSeekBar.getProgress()));
325 dialogIsVisible.set(false); // dialog is not on the screen
326 currentDialog.dismiss(); // hide the dialog
327 currentDialog = null; // dialog no longer needed
328 } // end method onClick
329 }; // end setColorButtonListener
330
OnSeekBarChangeListener
to Respond to the Events of the alpha
, red
, green
and blue SeekBar
sFigure 9.16 defines an anonymous inner class that implements interface OnSeekBarChangeListener
to respond to events when the user adjusts the SeekBar
s in the Choose Color Dialog
. This was registered as the SeekBar
s’ event handler in Fig. 9.15 (lines 246–249). Method onProgressChanged
(lines 270–290) is called when the position of a SeekBar
’s thumb changes. We retrieve from the currentDialog
each of the SeekBar
s and the View
used to display the color (lines 275–284). We then use class View
’s setBackgroundColor
method to update the colorView
with a Color
that matches the current state of the SeekBar
s (lines 287–289). Class Color
’s static
method argb
combines the SeekBar
s’ values into a Color
and returns the appropriate Color
. [Note: Method onProgressChanged
is called frequently when the user drags a SeekBar
’s thumb. For this reason, it’s better practice to get the GUI component references once and store them as instance variables in your class, rather than getting the references each time onProgressChanged
is called.]
OnClickListener
to Set the New Drawing ColorFigure 9.17 defines an anonymous inner class that implements interface OnClickListener
to set the new drawing color when the user clicks the Set Color Button
in the Choose Color Dialog
. This was registered as the Button
’s event handler in Fig. 9.15 (line 261). Method onClick
gets references to the SeekBar
s, then uses them in lines 322–324 to get the value from each SeekBar
and set the new drawing color. Line 325 indicates that a dialog is not displayed by calling isDialigVisible
’s set
method with the value false
. Line 326 calls the Dialog
’s dismiss
method to close the dialog and return to the app.
showLineWidthDialog
The showLineWidthDialog
method (Fig. 9.18) creates a Dialog
and sets its GUI by calling setContentView
to inflate width_dialog.xml
(lines 335–336). We also set the dialog’s title and indicate that it’s cancelable. Lines 341–344 get a reference to the dialog’s SeekBar
, set its OnSeekBarChangeListener
to the widthSeekBarChanged
listener (Fig. 9.19) and set its current value. Lines 347–349 get a reference to the dialog’s Button
and set its OnClickListener
to the setLineWidthButtonListener
(Fig. 9.20). Line 351 indicates that a dialog is displayed by calling isDialigVisible
’s set
method with the value true
. Finally, line 352 displays the dialog. The new line width is set only if the user touches the Set Line Width Button
in the Dialog
.
331 // display a dialog for setting the line width
332 private void showLineWidthDialog()
333 {
334 // create the dialog and inflate its content
335 currentDialog = new Dialog(this);
336 currentDialog.setContentView(R.layout.width_dialog);
337 currentDialog.setTitle(R.string.title_line_width_dialog);
338 currentDialog.setCancelable(true);
339
340 // get widthSeekBar and configure it
341 SeekBar widthSeekBar =
342 (SeekBar) currentDialog.findViewById(R.id.widthSeekBar);
343 widthSeekBar.setOnSeekBarChangeListener(widthSeekBarChanged);
344 widthSeekBar.setProgress(doodleView.getLineWidth());
345
346 // set the Set Line Width Button's onClickListener
347 Button setLineWidthButton =
348 (Button) currentDialog.findViewById(R.id.widthDialogDoneButton);
349 setLineWidthButton.setOnClickListener(setLineWidthButtonListener);
350
351 dialogIsVisible.set(true); // dialog is on the screen
352 currentDialog.show(); // show the dialog
353 } // end method showLineWidthDialog
354
355 // OnSeekBarChangeListener for the SeekBar in the width dialog
356 private OnSeekBarChangeListener widthSeekBarChanged =
357 new OnSeekBarChangeListener()
358 {
359 Bitmap bitmap = Bitmap.createBitmap( // create Bitmap
360 400, 100, Bitmap.Config.ARGB_8888);
361 Canvas canvas = new Canvas(bitmap); // associate with Canvas
362
363 @Override
364 public void onProgressChanged(SeekBar seekBar, int progress,
365 boolean fromUser)
366 {
367 // get the ImageView
368 ImageView widthImageView = (ImageView)
369 currentDialog.findViewById(R.id.widthImageView);
370
371 // configure a Paint object for the current SeekBar value
372 Paint p = new Paint();
373 p.setColor(doodleView.getDrawingColor());
374 p.setStrokeCap(Paint.Cap.ROUND);
375 p.setStrokeWidth(progress);
376
377 // erase the bitmap and redraw the line
378 bitmap.eraseColor(Color.WHITE);
379 canvas.drawLine(30, 50, 370, 50, p);
380 widthImageView.setImageBitmap(bitmap);
381 } // end method onProgressChanged
382
383 // required method of interface OnSeekBarChangeListener
384 @Override
385 public void onStartTrackingTouch(SeekBar seekBar)
386 {
387 } // end method onStartTrackingTouch
388
389 // required method of interface OnSeekBarChangeListener
390 @Override
391 public void onStopTrackingTouch(SeekBar seekBar)
392 {
393 } // end method onStopTrackingTouch
394 }; // end widthSeekBarChanged
395
396 // OnClickListener for the line width dialog's Set Line Width Button
397 private OnClickListener setLineWidthButtonListener =
398 new OnClickListener()
399 {
400 @Override
401 public void onClick(View v)
402 {
403 // get the color SeekBars
404 SeekBar widthSeekBar =
405 (SeekBar) currentDialog.findViewById(R.id.widthSeekBar);
406
407 // set the line color
408 doodleView.setLineWidth(widthSeekBar.getProgress());
409 dialogIsVisible.set(false); // dialog is not on the screen
410 currentDialog.dismiss(); // hide the dialog
411 currentDialog = null; // dialog no longer needed
412 } // end method onClick
413 }; // end setColorButtonListener
414 } // end class Doodlz
OnSeekBarChangeListener
to Respond to the Events of the widthSeekBar
Figure 9.19 defines the widthSeekBarChanged OnSeekBarChangeListener
that responds to events when the user adjusts the SeekBar
in the Choose Line Width Dialog
. Lines 359–360 create a Bitmap
on which to display a sample line representing the selected line thickness. Line 361 creates a Canvas
for drawing on the Bitmap
. Method onProgressChanged
(lines 364–381) draws the sample line based on the current drawing color and the SeekBar
’s value. First, lines 368–369 get a reference to the ImageView
where the line is displayed. Next, lines 372–375 configure a Paint
object for drawing the sample line. Class Paint
’s setStrokeCap
method (line 374) specifies the appearance of the line ends—in this case, they’re rounded (Paint.Cap.ROUND
). Line 378 clears bitmap
’s background to white with Bitmap
method eraseColor
. We use canvas
to draw the sample line. Finally, line 380 displays bitmap
in the widthImageView
by passing it to ImageView
’s setImageBitmap
method.
OnClickListener
to Respond to the Events of the Set Line Width Button
Figure 9.20 defines an anonymous inner class that implements interface OnClickListener
to set the new line width color when the user clicks the Set Line Width Button
in the Choose Line Width Dialog
. This was registered as the Button
’s event handler in Fig. 9.18 (line 349). Method onClick
gets a reference to Dialog
’s SeekBar
, then uses it to set the new line width based on the SeekBar
’s value. Line 409 indicates that a dialog is not displayed by calling isDialigVisible
’s set
method with the value false
. Line 410 calls the Dialog
’s dismiss
method to close the dialog and return to the app.
DoodleView
Subclass of View
Class DoodleView
(Figs. 9.21–9.29) processes the user’s touches and draws the corresponding lines.
1 // DoodleView.java
2 // Main View for the Doodlz app.
3 package com.deitel.doodlz;
4
5 import java.io.IOException;
6 import java.io.OutputStream;
7 import java.util.HashMap;
8
9 import android.content.ContentValues;
10 import android.content.Context;
11 import android.graphics.Bitmap;
12 import android.graphics.Canvas;
13 import android.graphics.Color;
14 import android.graphics.Paint;
15 import android.graphics.Path;
16 import android.graphics.Point;
17 import android.net.Uri;
18 import android.provider.MediaStore.Images;
19 import android.util.AttributeSet;
20 import android.view.Gravity;
21 import android.view.MotionEvent;
22 import android.view.View;
23 import android.widget.Toast;
24
DoodleView
Class for the Doodlz
App—The Main Screen That’s PaintedFigure 9.21 lists the package
and import
statements and the fields for class DoodleView
of the Doodlz app. The new classes and interfaces were discussed in Section 9.3 and are highlighted here.
DoodleView
Fields, Constructor and onSizeChanged
MethodClass DoodleView
’s fields (Fig. 9.22, lines 29–36) are used to manage the data for the set of lines that the user is currently drawing and to draw those lines. The constructor (lines 39–54) initializes the class’s fields. Line 43 creates the Paint
object paintScreen
that will be used to display the user’s drawing on the screen and line 46 creates the Paint
object paintLine
that specifies the settings for the line(s) the user is currently drawing. Lines 47–51 specify the settings for the paintLine
object. We pass true
to Paint
’s setAntiAlias
method to enable anti-aliasing which smooths the edges of the lines. Next, we set the Paint
’s style to Paint.Style.STROKE
with Paint
’s setStyle
method. The style can be STROKE
, FILL
or FILL_AND_STROKE
for a line, a filled shape without a border and a filled shape with a border, respectively. The default option is Paint.Style.FILL
. We set the line’s width using Paint
’s setStrokeWidth
method. This sets the app’s default line width to five pixels. We also use Paint
’s setStrokeCap
method to round the ends of the lines with Paint.Cap.ROUND
. Line 52 creates the pathMap
, which maps each finger ID (known as a pointer) to a corresponding Path
object for the lines currently being drawn. Line 53 creates the previousPointMap
, which maintains the last point for each finger—as each finger moves, we draw a line from its current point to its previous point.
25 // the main screen that is painted
26 public class DoodleView extends View
27 {
28 // used to determine whether user moved a finger enough to draw again
29 private static final float TOUCH_TOLERANCE = 10;
30
31 private Bitmap bitmap; // drawing area for display or saving
32 private Canvas bitmapCanvas; // used to draw on bitmap
33 private Paint paintScreen; // use to draw bitmap onto screen
34 private Paint paintLine; // used to draw lines onto bitmap
35 private HashMap<Integer, Path> pathMap; // current Paths being drawn
36 private HashMap<Integer, Point> previousPointMap; // current Points
37
38 // DoodleView constructor initializes the DoodleView
39 public DoodleView(Context context, AttributeSet attrs)
40 {
41 super(context, attrs); // pass context to View's constructor
42
43 paintScreen = new Paint(); // used to display bitmap onto screen
44
45 // set the initial display settings for the painted line
46 paintLine = new Paint();
47 paintLine.setAntiAlias(true); // smooth edges of drawn line
48 paintLine.setColor(Color.BLACK); // default color is black
49 paintLine.setStyle(Paint.Style.STROKE); // solid line
50 paintLine.setStrokeWidth(5); // set the default line width
51 paintLine.setStrokeCap(Paint.Cap.ROUND); // rounded line ends
52 pathMap = new HashMap<Integer, Path>();
53 previousPointMap = new HashMap<Integer, Point>();
54 } // end DoodleView constructor
55
56 // Method onSizeChanged creates BitMap and Canvas after app displays
57 @Override
58 public void onSizeChanged(int w, int h, int oldW, int oldH)
59 {
60 bitmap = Bitmap.createBitmap(getWidth(), getHeight(),
61 Bitmap.Config.ARGB_8888);
62 bitmapCanvas = new Canvas(bitmap);
63 bitmap.eraseColor(Color.WHITE); // erase the BitMap with white
64 } // end method onSizeChanged
65
The DoodleView
’s size is not determined until it’s inflated and added to the Doodlz Activity
’s View
hierarchy; therefore, we can’t determine the size of the drawing Bitmap
in onCreate
. So, lines 58–64 override View
method onSizeChanged
, which is called when the DoodleView
’s size changes—e.g., when it’s added to an Activity
’s View
hierarchy or when the user device rotates the device. In this app, onSizeChanged
is called only when the DoodleView
is added to the Doodlz Activity
’s View
hierarchy, because the app always displays in portrait mode (Fig. 9.4). Bitmap
’s static
createBitmap
method creates a Bitmap
of the specified width and height—here we use the DoodleView
’s width and height as the Bitmap
’s dimensions. The last argument to createBitmap
is the Bitmap
’s encoding, which specifies how each pixel in the Bitmap
is stored. The constant Bitmap.Config.ARGB_8888
indicates that each pixel’s color is stored in four bytes (one byte each for the alpha, red, green and blue values of the pixel’s color. Next, we create a new Canvas
that is used to draw shapes directly to the Bitmap
. Finally, we use Bitmap
’s eraseColor
method to fill the Bitmap
with white pixels—the default Bitmap
background is black.
clear
, setDrawingColor
, getDrawingColor
, setLineWidth
and getLineWidth
of Class DoodleView
Figure 9.23 defines methods clear
(lines 67–73), setDrawingColor
(lines 76–79), getDrawingColor
(lines 82–85), setLineWidth
(lines 88–91) and getLineWidth
(lines 94–97), which are called from the Doodlz Activity
. Method clear
empties the pathMap
and previousPointMap
, erases the Bitmap
by setting all of its pixels to white, then calls the inherited View
method invalidate
to indicate that the View
needs to be redrawn. Then, the system automatically determines when the View
’s onDraw
method should be called. Method setDrawingColor
changes the current drawing color by setting the color of the Paint
object paintLine
. Paint
’s setColor
method receives an int
that represents the new color in ARGB
format. Method getDrawingColor
returns the current color, which we use in the Choose Color Dialog
. Method setLineWidth
sets paintLine
’s stroke width to the specified number of pixels. Method getLineWidth
returns the current stroke width, which we use in the Choose Line Width Dialog
.
66 // clear the painting
67 public void clear()
68 {
69 pathMap.clear(); // remove all paths
70 previousPointMap.clear(); // remove all previous points
71 bitmap.eraseColor(Color.WHITE); // clear the bitmap
72 invalidate(); // refresh the screen
73 } // end method clear
74
75 // set the painted line's color
76 public void setDrawingColor(int color)
77 {
78 paintLine.setColor(color);
79 } // end method setDrawingColor
80
81 // return the painted line's color
82 public int getDrawingColor()
83 {
84 return paintLine.getColor();
85 } // end method getDrawingColor
86
87 // set the painted line's width
88 public void setLineWidth(int width)
89 {
90 paintLine.setStrokeWidth(width);
91 } // end method setLineWidth
92
93 // return the painted line's width
94 public int getLineWidth()
95 {
96 return (int) paintLine.getStrokeWidth();
97 } // end method getLineWidth
98
View
Method OnDraw
When a View
needs to be redrawn, it’s onDraw
method is called. Figure 9.24 overrides onDraw
to display bitmap
(the Bitmap
that contains the drawing) on the DoodleView
by calling the Canvas
argument’s drawBitmap
method. The first argument is the Bitmap
to draw, the next two arguments are the x-y coordinates where the upper-left corner of the Bitmap
should be placed on the View
and the last argument is the Paint
object that specifies the drawing characteristics. Lines 107–108 then loop through each Integer
key in the pathMap HashMap
. For each, we pass the corresponding Path
to Canvas
’s drawPath
method to draw each Path
to the screen using the paintLine
object, which defines the line width and color.
99 // called each time this View is drawn
100 @Override
101 protected void onDraw(Canvas canvas)
102 {
103 // draw the background screen
104 canvas.drawBitmap(bitmap, 0, 0, paintScreen);
105
106 // for each path currently being drawn
107 for (Integer key : pathMap.keySet())
108 canvas.drawPath(pathMap.get(key), paintLine); // draw line
109 } // end method onDraw
110
View
Method onTouchEvent
Method OnTouchEvent
(Fig. 9.25) is called when the View
receives a touch event. Android supports multitouch—that is, having multiple fingers touching the screen. The user can touch the screen with more fingers or remove fingers from the screen at any time. For this reason, each finger—known as a pointer—has a unique ID that identifies it across touch events. We’ll use that ID to locate the corresponding Path
objects that represent each line currently being drawn. These Path
s are stored in pathMap
.
111 // handle touch event
112 @Override
113 public boolean onTouchEvent(MotionEvent event)
114 {
115 // get the event type and the ID of the pointer that caused the event
116 int action = event.getActionMasked(); // event type
117 int actionIndex = event.getActionIndex(); // pointer (i.e., finger)
118
119 // determine which type of action the given MotionEvent
120 // represents, then call the corresponding handling method
121 if (action == MotionEvent.ACTION_DOWN ||
122 action == MotionEvent.ACTION_POINTER_DOWN)
123 {
124 touchStarted(event.getX(actionIndex), event.getY(actionIndex),
125 event.getPointerId(actionIndex));
126 } // end if
127 else if (action == MotionEvent.ACTION_UP ||
128 action == MotionEvent.ACTION_POINTER_UP)
129 {
130 touchEnded(event.getPointerId(actionIndex));
131 } // end else if
132 else
133 {
134 touchMoved(event);
135 } // end else
136
137 invalidate(); // redraw
138 return true; // consume the touch event
139 } // end method onTouchEvent
140
MotionEvent
’s getActionMasked
method (line 116) returns an int
representing the MotionEvent
type, which you can use with constants from class MotionEvent
to determine how to handle each event. MotionEvent
’s getActionIndex
method returns an integer index representing which finger caused the event. This index is not the finger’s unique ID—it’s simply the index at which that finger’s information is located in this MotionEvent
object. To get the finger’s unique ID that persists across MotionEvent
s until the user removes that finger from the screen, we’ll use MotionEvent
’s getPointerID
method (lines 125 and 130), passing the finger index as an argument.
If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
(lines 121–122), the user touched the screen with a new finger. The first finger to touch the screen generates a MotionEvent.ACTION_DOWN
event, and all other fingers generate MotionEvent.ACTION_POINTER_DOWN
events. For these cases, we call the touchStarted
method (Fig. 9.26) to store the initial coordinates of the touch. If the action is MotionEvent.ACTION_UP
or MotionEvent.ACTION_POINTER_UP
, the user removed a finger from the screen, so we call method touchEnded
(Fig. 9.28) to draw the completed Path
to the bitmap
so that we have a permanent record of that Path
. For all other touch events, we call method touchMoved
(Fig. 9.27) to draw the lines. After the event is processed, line 137 calls the inherited View
method invalidate
to redraw the screen, and line 138 returns true
to indicate that the event has been processed.
141 // called when the user touches the screen
142 private void touchStarted(float x, float y, int lineID)
143 {
144 Path path; // used to store the path for the given touch id
145 Point point; // used to store the last point in path
146
147 // if there is already a path for lineID
148 if (pathMap.containsKey(lineID))
149 {
150 path = pathMap.get(lineID); // get the Path
151 path.reset(); // reset the Path because a new touch has started
152 point = previousPointMap.get(lineID); // get Path's last point
153 } // end if
154 else
155 {
156 path = new Path(); // create a new Path
157 pathMap.put(lineID, path); // add the Path to Map
158 point = new Point(); // create a new Point
159 previousPointMap.put(lineID, point); // add the Point to the Map
160 } // end else
161
162 // move to the coordinates of the touch
163 path.moveTo(x, y);
164 point.x = (int) x;
165 point.y = (int) y;
166 } // end method touchStarted
167
168 // called when the user drags along the screen
169 private void touchMoved(MotionEvent event)
170 {
171 // for each of the pointers in the given MotionEvent
172 for (int i = 0; i < event.getPointerCount(); i++)
173 {
174 // get the pointer ID and pointer index
175 int pointerID = event.getPointerId(i);
176 int pointerIndex = event.findPointerIndex(pointerID);
177
178 // if there is a path associated with the pointer
179 if (pathMap.containsKey(pointerID))
180 {
181 // get the new coordinates for the pointer
182 float newX = event.getX(pointerIndex);
183 float newY = event.getY(pointerIndex);
184
185 // get the Path and previous Point associated with
186 // this pointer
187 Path path = pathMap.get(pointerID);
188 Point point = previousPointMap.get(pointerID);
189
190 // calculate how far the user moved from the last update
191 float deltaX = Math.abs(newX - point.x);
192 float deltaY = Math.abs(newY - point.y);
193
194 // if the distance is significant enough to matter
195 if (deltaX >= TOUCH_TOLERANCE || deltaY >= TOUCH_TOLERANCE)
196 {
197 // move the path to the new location
198 path.quadTo(point.x, point.y, (newX + point.x) / 2,
199 (newY + point.y) / 2);
200
201 // store the new coordinates
202 point.x = (int) newX;
203 point.y = (int) newY;
204 } // end if
205 } // end if
206 } // end for
207 } // end method touchMoved
208
209 // called when the user finishes a touch
210 private void touchEnded(int lineID)
211 {
212 Path path = pathMap.get(lineID); // get the corresponding Path
213 bitmapCanvas.drawPath(path, paintLine); // draw to bitmapCanvas
214 path.reset(); // reset the Path
215 } // end method touchEnded
216
touchStarted
Method of Class DoodleView
The utility method touchStarted
(Fig. 9.26) is called when a finger first touches the screen. The coordinates of the touch and its ID are supplied as arguments. If a Path
already exists for the given ID (line 148), we call Path
’s reset
method to clear any existing points so we can reuse the Path
for a new stroke. Otherwise, we create a new Path
, add it to pathMap
, then add a new Point
to the previousPointMap
. Lines 163–165 call Path
’s moveTo
method to set the Path
’s starting coordinates and specify the new Point
’s x
and y
values.
touchMoved
Method of Class DoodleView
The utility method touchMoved
(Fig. 9.27) is called when the user moves one or more fingers across the screen. The system MotionEvent
passed from onTouchEvent
contains touch information for multiple moves on the screen if they occur at the same time. MotionEvent
method getPointerCount
(line 172) returns the number of touches this MotionEvent
describes. For each, we store the finger’s ID (line 175) in pointerID
, and store the finger’s corresponding index in this MotionEvent
(line 176) in pointerIndex
. Then we check whether there’s a corresponding Path
in the pathMap HashMap
(line 179). If so, we use MotionEvent
’s getX
and getY
methods to get the last coordinates for this drag event for the specified pointerIndex
. We get the corresponding Path
and last Point
for the pointerID
from each respective HashMap
, then calculate the difference between the last point and the current point—we want to update the Path
only if the user has moved a distance that’s greater than our TOUCH_TOLERANCE
constant. We do this because many devices are sensitive enough to generate MotionEvent
s indicating small movements when the user is attempting to hold a finger motionless on the screen. If the user moved a finger further than the TOUCH_TOLERANCE
, we use Path
’s quadTo
method (lines 198–199) to add a geometric curve (specifically a quadratic bezier curve) from the previous Point
to the new Point
. We then update the most recent Point
for that finger.
touchEnded
Method of Class DoodleView
The utility method touchEnded
(Fig. 9.28) is called when the user lifts a finger from the screen. The method receives the ID of the finger (lineID
) for which the touch just ended as an argument. Line 212 gets the corresponding Path
. Line 213 calls the bitmapCanvas
’s drawPath
method to draw the Path
on the Bitmap
object named bitmap
before we call Path
’s reset
method to clear the Path
. Resetting the Path
does not erase its corresponding painted line from the screen, because those lines have already been drawn to the bitmap
that’s displayed to the screen. The lines that are currently being drawn by the user are displayed on top of that bitmap
.
saveImage
MethodThe saveImage
method (Fig. 9.29) saves the current drawing to a file in the device’s gallery. [Note: It’s possible that the image will not immediately appear in the gallery. For example, Android scans storage for new media items like images, videos and music when a device is first powered on. Some devices scan for new media in the background. In an AVD, you can run the AVD’s Dev Tools app and touch its Media Scanner option, then the new image will appear in the gallery.]
217 // save the current image to the Gallery
218 public void saveImage()
219 {
220 // use "Doodlz" followed by current time as the image file name
221 String fileName = "Doodlz" + System.currentTimeMillis();
222
223 // create a ContentValues and configure new image's data
224 ContentValues values = new ContentValues();
225 values.put(Images.Media.TITLE, fileName);
226 values.put(Images.Media.DATE_ADDED, System.currentTimeMillis());
227 values.put(Images.Media.MIME_TYPE, "image/jpg");
228
229 // get a Uri for the location to save the file
230 Uri uri = getContext().getContentResolver().insert(
231 Images.Media.EXTERNAL_CONTENT_URI, values);
232
233 try
234 {
235 // get an OutputStream to uri
236 OutputStream outStream =
237 getContext().getContentResolver().openOutputStream(uri);
238
239 // copy the bitmap to the OutputStream
240 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
241
242 // flush and close the OutputStream
243 outStream.flush(); // empty the buffer
244 outStream.close(); // close the stream
245
246 // display a message indicating that the image was saved
247 Toast message = Toast.makeText(getContext(),
248 R.string.message_saved, Toast.LENGTH_SHORT);
249 message.setGravity(Gravity.CENTER, message.getXOffset() / 2,
250 message.getYOffset() / 2);
251 message.show(); // display the Toast
252 } // end try
253 catch (IOException ex)
254 {
255 // display a message indicating that the image was saved
256 Toast message = Toast.makeText(getContext(),
257 R.string.message_error_saving, Toast.LENGTH_SHORT);
258 message.setGravity(Gravity.CENTER, message.getXOffset() / 2,
259 message.getYOffset() / 2);
260 message.show(); // display the Toast
261 } // end catch
262 } // end method saveImage
263 } // end class DoodleView
We use "Doodlz"
followed by current time as the image’s file name. Line 224 creates a new ContentValues
object, which will be used by a ContentResolver
to specify the image’s title (i.e., file name), the date the image was created and the MIME type of the image ("image/jpg"
in this example). For more information on MIME types, visit
www.w3schools.com/media/media_mimeref.asp
ContentValues
method put
adds a key-value pair to a ContentValues
object. The key Images.Media.TITLE
(line 225) is used to specify fileName
as the image file name. The key Images.Media.DATE_ADDED
(line 226) is used to specify the time when this file was saved to the device. The key Images.Media.MIME_TYPE
(line 227) is used to specify the file’s MIME type as a JPEG image.
Lines 230–231 get this app’s ContentResolver
, then call its insert
method to get a Uri
where the image will be stored. The constant Images.Media.EXTERNAL_CONTENT_URI
indicates that we want to store the image on the device’s external storage device—typically an SD card if one is available. We pass our ContentValues
as the second argument to create a file with our supplied file name, creation date and MIME type. Once the file is created we can write the screenshot to the location provided by the returned Uri
. To do so, we get an OutputStream
that allows us to write to the specified Uri
(lines 236–237). Next, we invoke class Bitmap
’s compress
method, which receives a constant representing the compression format (Bitmap.CompressFormat.JPEG
), an integer representing the quality (100 indicates the best quality image) and the OutputStream
where the image’s bytes should be written. Then lines 243–244 flush and close the OutputStream
, respectively.
If the file is saved successfully, we use a Toast
to indicate that the image was saved (lines 247–251); otherwise, we use a Toast
to indicate that there was an error when saving the image (lines 256–260). Toast
method makeText
receives as arguments the Context
on which the Toast
is displayed, the message to display and the duration for which the Toast
will be displayed. Toast
method setGravity
specifies where the Toast
will appear. The constant Gravity.CENTER
indicates that the Toast
should be centered over the coordinates specified by the method’s second and third arguments. Toast
method show
displays the Toast
.
In this app, you learned how to turn a device’s screen into a virtual canvas. You set the app’s target SDK to "11"
to enable a pre-Android 3.0 app to use Android 3.0’s holographic user interface components and to integrate the app menu into Android 3.0’s action bar, when the app runs on an Android 3.0 device. You processed sensor events—such as those generated by a device’s accelerometer—by registering a SensorEventListener
with the system’s SensorManager
service. We displayed dialogs with complex GUIs in objects of class Dialog
. We also used a thread-safe AtomicBoolean
to help determine when a dialog was already on the screen so that our sensor event handler would not display another dialog.
You learned how to create custom ARGB Color
s with alpha, red, green and blue components and how to extract those individual components from an existing Color
. We drew lines onto Bitmap
s using associated Canvas
objects, then displayed those Bitmap
s on the screen. You also saved a Bitmap
as an image in the device’s gallery.
As the user dragged one or more fingers on the screen, we stored the information for each finger as a Path
. We processed the touch events by overriding the View
method onTouchEvent
and using its MotionEvent
parameter to get the type of touch event that occurred and the ID of the finger that generated the event.
You learned how to save an image into the device’s gallery by getting an OutputStream
from a ContentResolver
. Finally, you used a Toast
to display a message that automatically disappears after a short period of time.
In Chapter 10, we build the Address Book app, which provides quick and easy access to stored contact information and the ability to delete contacts, add contacts and edit existing contacts. The user can scroll through an alphabetical contact list, add contacts and view more information about individual contacts. Touching a contact’s name displays a screen showing the contact’s detailed information.
9.1. Fill in the blanks in each of the following statements:
a. Android 3.0’s look-and-feel is called the __________.
b. You use the SensorManager
to register the sensor changes that your app should receive and to specify the __________ that will handle those sensor-change events.
c. A Path
object (package android.graphics
) represents a geometric path consisting of line segments and __________.
d. You use the type of the touch event to determine whether the user has touched the screen, __________ or lifted a finger from the screen.
e. Use class SensorManager
’s __________ method to stop listening for accelerometer events.
f. Override SensorEventListener
method __________ to process accelerometer events.
g. Use Dialog
’s __________ method to close a dialog.
h. When a View
needs to be redrawn, its __________ method is called.
i. MotionEvent
’s __________ method returns an int
representing the MotionEvent
type, which you can use with constants from class MotionEvent
to determine how to handle each event.
j. The utility method __________ is called when the user moves one or more fingers across the screen.
k. Toast
method __________ receives as arguments the Context
on which the Toast
is displayed, the message to display and the duration for which the Toast
will be displayed.
9.2. State whether each of the following is true or false. If false, explain why.
a. We use the standard SeekBar
thumb in our apps, but you can customize it by setting the SeekBar
’s android:seekBar
attribute to a drawable resource, such as an image.
b. You unregister the accelerometer event handler when the app is sent to the foreground.
c. Call the inherited View
method validate
to indicate that the View
needs to be redrawn.
d. If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
, the user touched the screen with the same finger.
e. Resetting the Path
erases its corresponding painted line from the screen, because those lines have already been drawn to the bitmap
that’s displayed to the screen.
a. holographic theme
.
b. SensorEventListener
.
c. curves.
d. dragged across the screen.
e. unregisterListener
.
f. onSensorChanged
.
g. dismiss
.
h. onDraw
.
i. getActionMasked
.
j. touchMoved
.
k. makeText
.
a. False. We use the standard SeekBar
thumb in our apps, but you can customize it by setting the SeekBar
’s android:thumb
attribute to a drawable resource, such as an image.
b. False. You unregister the accelerometer event handler when the app is sent to the background.
c. False. Call the inherited View
method invalidate
to indicate that the View
needs to be redrawn.
d. False. If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
, the user touched the screen with a new finger.
e. False. Resetting the Path
does not erase its corresponding painted line from the screen, because those lines have already been drawn to the bitmap
that’s displayed to the screen.
9.3. Fill in the blanks in each of the following statements:
a. Most Android devices have a(n) __________ that allows apps to detect movement.
b. A(n) __________ (package android.widget
) displays a message for a short time, then disappears from the screen.
c. The __________ monitors the accelerometer to detect device movement.
d. SensorManager
’s __________ constant represents the acceleration due to gravity on earth.
e. You register to receive accelerometer events using SensorManager
’s registerListener
method, which receives three arguments: the SensorEventListener
object that will respond to the events, a Sensor
representing the type of sensor data the app wishes to receive and __________.
f. You pass true
to Paint
’s __________ method to enable anti-aliasing which smooths the edges of the lines.
g. Method __________ sets paintLine
’s stroke width to the specified number of pixels.
h. Android supports __________—that is, having multiple fingers touching the screen.
i. Utility method __________ is called when the user lifts a finger from the screen. The method receives the ID of the finger for which the touch just ended as an argument.
9.4 State whether each of the following is true or false. If false, explain why.
a. In Android, sensor events are handled in the GUI thread.
b. The alpha component specifies the Color
’s transparency with 0 representing completely transparent and 100 representing completely opaque.
c. For accelerometer events, the SensorEvent
parameter values
array contains three elements representing the acceleration (in meter/second2) in the x (left/right), y (up/down) and z (forward/backward) directions.
d. Method onProgressChanged
is called once when the user drags a SeekBar
’s thumb.
e. To get the finger’s unique ID that persists across MotionEvent
s until the user removes that finger from the screen, you use MotionEvent
’s getID
method, passing the finger index as an argument.
f. The system MotionEvent
passed from onTouchEvent
contains touch information for multiple moves on the screen if they occur at the same time.
g. Toast
method setLocation
specifies where the Toast
will appear. The constant Gravity.CENTER
indicates that the Toast
should be centered over the coordinates specified by the method’s second and third arguments. Toast
method show
displays the Toast
.
h. Use a ToastMessage
to display a message that automatically disappears after a short period of time.
9.5 (Enhanced Doodlz App) Make the following enhancements to the Doodlz app:
a. Allow the user to select a background color. The erase capability should use the selected background color. Clearing the entire image should return the background to the default white background.
b. Allow the user to select a background image on which to draw. Clearing the entire image should return the background to the default white background. The erase capability should use the default white background color.
c. Use pressure to determine transparency of color or thickness of line. Class MotionEvent
has methods that allow you to get the pressure of the touch.
d. Add the ability to draw rectangles and ovals. Options should include whether the shape is filled or hollow. The user should be able to specify the line thickness for each shape’s border and the shape’s fill color.
e. Advanced: When the user selects a background image on which to draw, the erase capability should reveal the original background image pixels in the erased location.
9.6. (Hangman Game App) Recreate the classic word game Hangman using the Android robot icon rather than a stick figure. (For the Android logo terms of use, visit www.android.com/branding.html). At the start of the game, display a dashed line with one dash representing each letter in the word. As a hint to the user, provide either a category for the word (e.g., sport or landmark) or the word’s definition. Ask the user to enter a letter. If the letter is in the word, place it in the location of the corresponding dash. If the letter is not part of the word, draw part of the Android robot on the screen (e.g., the robot’s head). For each incorrect answer, draw another part of the Android robot. The game ends when the user completes the word or the entire Android Robot is drawn to the screen.
9.7. (Fortune Teller App) The user “asks a question” then shakes the phone to find a fortune (e.g., “probably not,” “looks promising,” “ask me again later.” etc.
9.8. (Block Breaker Game) Display several columns of blocks in red, yellow, blue and green. Each column should have blocks of each color randomly placed. Blocks can be removed from the screen only if they are in groups of two or more. A group consists of blocks of the same color that are vertically and/or horizontally adjacent. When the user taps a group of blocks, the group disappears and the blocks above move down to fill the space. The goal is to clear all of the blocks from the screen. More points should be awarded for larger groups of blocks.
9.9. (Enhanced Block Breaker Game) Modify the Block Breaker game in Exercise 9.8 as follows:
a. Provide a timer—the user wins by clearing the blocks in the alotted time. Add more blocks to the screen the longer it takes the user to clear the screen.
b. Add multiple levels. In each level, the alotted time for clearing the screen decreases.
c. Provide a continous mode in which as the user clears blocks, a new row of blocks is added. If the space below a given block is empty, the block should drop into that space. In this mode, the game ends when the user cannot remove any more blocks.
d. Keep track of the high scores in each game mode.
9.10. (Word Search App) Create a grid of letters that fills the screen. Hidden in the grid should be at least ten words. The words may be horizontal, vertical or diagonal, and, in each case, forwards, backwards, up or down. Allow the user to highlight the words by dragging a finger across the letters on the screen or tapping each letter of the word. Include a timer. The less time it takes the user to complete the game, the higher the score. Keep track of the high scores.
9.11. (Fractal App) Research how to draw fractals and develop an app that draws them. Provide options that allow the user to control the number of levels of the fractal and its colors.
9.12. (Kaleidascope App) Create an app that simulates a kaleidascope. Allow the user to shake the device to redraw the screen.
9.13. (Labyrinth Game App: Open Source) Check out the open-source Android app, Amazed, on the Google Code site (http://apps-for-android.googlecode.com/svn/trunk/Amazed/). In this game, the user maneuvers a marble through a maze by tilting the device in various directions. Possible modifications and enhancements include: adding a timer to keep track of how fast the user completes the game, improving the graphics, adding sounds and adding more puzzles of varying difficulty.
9.14. (Game of Snake App) Research the Game of Snake online and develop an app that allows a user to play the game.
18.224.33.107