The DoodleView
class (Sections 5.8.1–5.8.12) processes the user’s touches and draws the corresponding lines.
Figure 5.31 lists class DoodleView
’s package
statement and import
statements. The new classes and interfaces are highlighted here. Many of these were discussed in Section 5.3, and the rest are discussed as we use them throughout class DoodleView
.
1 // DoodleView.java
2 // Main View for the Doodlz app.
3 package com.deitel.doodlz;
4
5 import android.content.Context;
6 import android.graphics.Bitmap;
7 import android.graphics.Canvas;
8 import android.graphics.Color;
9 import android.graphics.Paint;
10 import android.graphics.Path;
11 import android.graphics.Point;
12 import android.provider.MediaStore;
13 import android.support.v4.print.PrintHelper;
14 import android.util.AttributeSet;
15 import android.view.Gravity;
16 import android.view.MotionEvent;
17 import android.view.View;
18 import android.widget.Toast;
19
20 import java.util.HashMap;
21 import java.util.Map;
22
Class DoodleView
’s static
and instance variables (Fig. 5.32) are used to manage the data for the set of lines that the user is currently drawing and to draw those lines. Line 34 creates the pathMap
, which maps each finger ID (known as a pointer) to a corresponding Path
object for the lines currently being drawn. Line 35 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. We discuss the other fields as we use them in class DoodleView
.
23 // custom View for drawing
24 public class DoodleView extends View {
25 // used to determine whether user moved a finger enough to draw again
26 private static final float TOUCH_TOLERANCE = 10;
27
28 private Bitmap bitmap; // drawing area for displaying or saving
29 private Canvas bitmapCanvas; // used to to draw on the bitmap
30 private final Paint paintScreen; // used to draw bitmap onto screen
31 private final Paint paintLine; // used to draw lines onto bitmap
32
33 // Maps of current Paths being drawn and Points in those Paths
34 private final Map<Integer, Path> pathMap = new HashMap<>();
35 private final Map<Integer, Point> previousPointMap = new HashMap<>();
36
The constructor (Fig. 5.33) initializes several of the class’s instance variables—the two Map
s are initialized in their declarations in Fig. 5.32. Line 40 of Fig. 5.33 creates the Paint
object paintScreen
that will be used to display the user’s drawing on the screen, and line 43 creates the Paint
object paintLine
that specifies the settings for the line(s) the user is currently drawing. Lines 44–48 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
.
37 // DoodleView constructor initializes the DoodleView
38 public DoodleView(Context context, AttributeSet attrs) {
39 super(context, attrs); // pass context to View's constructor
40 paintScreen = new Paint(); // used to display bitmap onto screen
41
42 // set the initial display settings for the painted line
43 paintLine = new Paint();
44 paintLine.setAntiAlias(true); // smooth edges of drawn line
45 paintLine.setColor(Color.BLACK); // default color is black
46 paintLine.setStyle(Paint.Style.STROKE); // solid line
47 paintLine.setStrokeWidth(5); // set the default line width
48 paintLine.setStrokeCap(Paint.Cap.ROUND); // rounded line ends
49 }
50
The DoodleView
’s size is not determined until it’s inflated and added to the MainActivity
’s View
hierarchy; therefore, we can’t determine the size of the drawing Bitmap
in onCreate
. So, we override View
method onSizeChanged
(Fig. 5.34), 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 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 on phones and small tablets, and in landscape on large tablets.
Software Engineering Observation 5.3
In apps that support both portrait and landscape orientations, onSizeChanged
is called each time the user rotates the device. In this app, that would result in a new Bitmap
each tim the method is called. When replacing a Bitmap
, you should call the prior Bitmap
’s recycle
method to release its resources.
51 // creates Bitmap and Canvas based on View's size
52 @Override
53 public void onSizeChanged(int w, int h, int oldW, int oldH) {
54 bitmap = Bitmap.createBitmap(getWidth(), getHeight(),
55 Bitmap.Config.ARGB_8888);
56 bitmapCanvas = new Canvas(bitmap);
57 bitmap.eraseColor(Color.WHITE); // erase the Bitmap with white
58 }
59
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’s 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.
Figure 5.35 defines methods clear
(lines 61–66), setDrawingColor
(lines 69–71), getDrawingColor
(lines 74–76), setLineWidth
(lines 79–81) and getLineWidth
(lines 84–86), which are called from the MainActivityFragment
. Method clear
, which we use in the EraseImageDialogFragment
, 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 ColorDialogFragment
. 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 LineWidthDialogFragment
.
60 // clear the painting
61 public void clear() {
62 pathMap.clear(); // remove all paths
63 previousPointMap.clear(); // remove all previous points
64 bitmap.eraseColor(Color.WHITE); // clear the bitmap
65 invalidate(); // refresh the screen
66 }
67
68 // set the painted line's color
69 public void setDrawingColor(int color) {
70 paintLine.setColor(color);
71 }
72
73 // return the painted line's color
74 public int getDrawingColor() {
75 return paintLine.getColor();
76 }
77
78 // set the painted line's width
79 public void setLineWidth(int width) {
80 paintLine.setStrokeWidth(width);
81 }
82
83 // return the painted line's width
84 public int getLineWidth() {
85 return (int) paintLine.getStrokeWidth();
86 }
87
When a View
needs to be redrawn, its onDraw method is called. Figure 5.36 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 95–96 then loop through and display the Path
s that are currently being drawn. For each Integer
key in the pathMap
, we pass the corresponding Path
to Canvas
’s drawPath method to draw the Path
using the paintLine
object, which defines the line width and color.
88 // perform custom drawing when the DoodleView is refreshed on screen
89 @Override
90 protected void onDraw(Canvas canvas) {
91 // draw the background screen
92 canvas.drawBitmap(bitmap, 0, 0, paintScreen);
93
94 // for each path currently being drawn
95 for (Integer key : pathMap.keySet())
96 canvas.drawPath(pathMap.get(key), paintLine); // draw line
97 }
98
Method onTouchEvent
(Fig. 5.37) is called when the View
receives a touch event. Android supports multitouch—that is, having multiple fingers touching the screen. At any time, the user can touch the screen with more fingers or remove fingers from the screen. 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
.
99 // handle touch event
100 @Override
101 public boolean onTouchEvent(MotionEvent event) {
102 int action = event.getActionMasked(); // event type
103 int actionIndex = event.getActionIndex(); // pointer (i.e., finger)
104
105 // determine whether touch started, ended or is moving
106 if (action == MotionEvent.ACTION_DOWN ||
107 action == MotionEvent.ACTION_POINTER_DOWN) {
108 touchStarted(event.getX(actionIndex), event.getY(actionIndex),
109 event.getPointerId(actionIndex));
110 }
111 else if (action == MotionEvent.ACTION_UP ||
112 action == MotionEvent.ACTION_POINTER_UP) {
113 touchEnded(event.getPointerId(actionIndex));
114 }
115 else {
116 touchMoved(event);
117 }
118
119 invalidate(); // redraw
120 return true;
121 }
122
MotionEvent
’s getActionMasked method (line 102) 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 (line 103) 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 109 and 113), passing the finger index as an argument.
If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
(lines 106–107), 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. 5.38) 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. 5.40) 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. 5.39) to draw the lines. After the event is processed, line 119 (of Fig. 5.37) calls the inherited View
method invalidate
to redraw the screen, and line 120 returns true
to indicate that the event has been processed.
The touchStarted
method (Fig. 5.38) 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 129), 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 142–144 call Path
’s moveTo method to set the Path
’s starting coordinates and specify the new Point
’s x
and y
values.
123 // called when the user touches the screen
124 private void touchStarted(float x, float y, int lineID) {
125 Path path; // used to store the path for the given touch id
126 Point point; // used to store the last point in path
127
128 // if there is already a path for lineID
129 if (pathMap.containsKey(lineID)) {
130 path = pathMap.get(lineID); // get the Path
131 path.reset(); // resets the Path because a new touch has started
132 point = previousPointMap.get(lineID); // get Path's last point
133 }
134 else {
135 path = new Path();
136 pathMap.put(lineID, path); // add the Path to Map
137 point = new Point(); // create a new Point
138 previousPointMap.put(lineID, point); // add the Point to the Map
139 }
140
141 // move to the coordinates of the touch
142 path.moveTo(x, y);
143 point.x = (int) x;
144 point.y = (int) y;
145 }
146
The touchMoved
method (Fig. 5.39) 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 150) returns the number of touches this MotionEvent
describes. For each, we store the finger’s ID (line 152) in pointerID
, and store the finger’s corresponding index in this MotionEvent
(line 153) in pointerIndex
. Then we check whether there’s a corresponding Path
in pathMap
(line 156). 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 173–174) 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.
147 // called when the user drags along the screen
148 private void touchMoved(MotionEvent event) {
149 // for each of the pointers in the given MotionEvent
150 for (int i = 0; i < event.getPointerCount(); i++) {
151 // get the pointer ID and pointer index
152 int pointerID = event.getPointerId(i);
153 int pointerIndex = event.findPointerIndex(pointerID);
154
155 // if there is a path associated with the pointer
156 if (pathMap.containsKey(pointerID)) {
157 // get the new coordinates for the pointer
158 float newX = event.getX(pointerIndex);
159 float newY = event.getY(pointerIndex);
160
161 // get the path and previous point associated with
162 // this pointer
163 Path path = pathMap.get(pointerID);
164 Point point = previousPointMap.get(pointerID);
165
166 // calculate how far the user moved from the last update
167 float deltaX = Math.abs(newX - point.x);
168 float deltaY = Math.abs(newY - point.y);
169
170 // if the distance is significant enough to matter
171 if (deltaX >= TOUCH_TOLERANCE || deltaY >= TOUCH_TOLERANCE) {
172 // move the path to the new location
173 path.quadTo(point.x, point.y, (newX + point.x) / 2,
174 (newY + point.y) / 2);
175
176 // store the new coordinates
177 point.x = (int) newX;
178 point.y = (int) newY;
179 }
180 }
181 }
182 }
183
The touchEnded
method (Fig. 5.40) 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 186 gets the corresponding Path
. Line 187 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
.
184 // called when the user finishes a touch
185 private void touchEnded(int lineID) {
186 Path path = pathMap.get(lineID); // get the corresponding Path
187 bitmapCanvas.drawPath(path, paintLine); // draw to bitmapCanvas
188 path.reset(); // reset the Path
189 }
190
Method saveImage
(Fig. 5.41) saves the current drawing. Line 194 creates a filename for the image, then lines 197–199 store the image in the device’s Photos app by calling class MediaStore.Images.Media
’s insertImage
method. The method receives four arguments:
• a ContentResolver
that the method uses to locate where the image should be stored on the device
• the Bitmap
to store
• a description of the image
Method insertImage
returns a String
representing the image’s location on the device, or null if the image could not be saved. Lines 201–217 check whether the image was saved and display an appropriate Toast
.
191 // save the current image to the Gallery
192 public void saveImage() {
193 // use "Doodlz" followed by current time as the image name
194 final String name = "Doodlz" + System.currentTimeMillis() + ".jpg";
195
196 // insert the image on the device
197 String location = MediaStore.Images.Media.insertImage(
198 getContext().getContentResolver(), bitmap, name,
199 "Doodlz Drawing");
200
201 if (location != null) {
202 // display a message indicating that the image was saved
203 Toast message = Toast.makeText(getContext(),
204 R.string.message_saved,
205 Toast.LENGTH_SHORT);
206 message.setGravity(Gravity.CENTER, message.getXOffset() / 2,
207 message.getYOffset() / 2);
208 message.show();
209 }
210 else {
211 // display a message indicating that there was an error saving
212 Toast message = Toast.makeText(getContext(),
213 R.string.message_error_saving, Toast.LENGTH_SHORT);
214 message.setGravity(Gravity.CENTER, message.getXOffset() / 2,
215 message.getYOffset() / 2);
216 message.show();
217 }
218 }
219
Method printImage
(Fig. 5.42) uses the Android Support Library’s PrintHelper
class to print the current drawing—this is available only on devices running Android 4.4 or higher. Line 222 first confirms that printing support is available on the device. If so, line 224 creates a PrintHelper
object. Next, line 227 specifies the image’s scale mode—PrintHelper.SCALE_MODE_FIT
indicates that the image should fit within the printable area of the paper. There’s also the scale mode PrintHelper.SCALE_MODE_FILL
, which causes the image to fill the paper, possibly cutting off a portion of the image. Finally, line 228 calls PrintHelper
method printBitmap, passing as arguments the print job name (used by the printer to identify the print) and the Bitmap
containing the image to print. This displays Android’s print dialog, which allows the user to choose whether to save the image as a PDF document on the device or to print it to an available printer.
220 // print the current image
221 public void printImage() {
222 if (PrintHelper.systemSupportsPrint()) {
223 // use Android Support Library's PrintHelper to print image
224 PrintHelper printHelper = new PrintHelper(getContext());
225
226 // fit image in page bounds and print the image
227 printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT);
228 printHelper.printBitmap("Doodlz Image", bitmap);
229 }
230 else {
231 // display message indicating that system does not allow printing
232 Toast message = Toast.makeText(getContext(),
233 R.string.message_error_printing, Toast.LENGTH_SHORT);
234 message.setGravity(Gravity.CENTER, message.getXOffset() / 2,
235 message.getYOffset() / 2);
236 message.show();
237 }
238 }
239 }
Class ColorDialogFragment
(Figs. 5.43–5.47) extends DialogFragment
to create an AlertDialog
for setting the drawing color. The class’s instance variables (lines 18–23) are used to reference the GUI controls for selecting the new color, displaying a preview of it and storing the color as a 32-bit int
value that represents the color’s ARGB values.
1 // ColorDialogFragment.java
2 // Allows user to set the drawing color on the DoodleView
3 package com.deitel.doodlz;
4
5 import android.app.Activity;
6 import android.app.AlertDialog;
7 import android.app.Dialog;
8 import android.content.DialogInterface;
9 import android.graphics.Color;
10 import android.os.Bundle;
11 import android.support.v4.app.DialogFragment;
12 import android.view.View;
13 import android.widget.SeekBar;
14 import android.widget.SeekBar.OnSeekBarChangeListener;
15
16 // class for the Select Color dialog
17 public class ColorDialogFragment extends DialogFragment {
18 private SeekBar alphaSeekBar;
19 private SeekBar redSeekBar;
20 private SeekBar greenSeekBar;
21 private SeekBar blueSeekBar;
22 private View colorView;
23 private int color;
24
Method onCreateDialog
(Fig. 5.44) inflates the custom View
(lines 31–32) defined by fragment_color.xml
containing the GUI for selecting a color, then attaches that View
to the AlertDialog
by calling AlertDialog.Builder
’s setView
method (line 33). Lines 39–47 get references to the dialog’s SeekBar
s and colorView
. Next, lines 50–53 register colorChangedListener
(Fig. 5.47) as the listener for the SeekBar
s’ events.
25 // create an AlertDialog and return it
26 @Override
27 public Dialog onCreateDialog(Bundle bundle) {
28 // create dialog
29 AlertDialog.Builder builder =
30 new AlertDialog.Builder(getActivity());
31 View colorDialogView = getActivity().getLayoutInflater().inflate(
32 R.layout.fragment_color, null);
33 builder.setView(colorDialogView); // add GUI to dialog
34
35 // set the AlertDialog's message
36 builder.setTitle(R.string.title_color_dialog);
37
38 // get the color SeekBars and set their onChange listeners
39 alphaSeekBar = (SeekBar) colorDialogView.findViewById(
40 R.id.alphaSeekBar);
41 redSeekBar = (SeekBar) colorDialogView.findViewById(
42 R.id.redSeekBar);
43 greenSeekBar = (SeekBar) colorDialogView.findViewById(
44 R.id.greenSeekBar);
45 blueSeekBar = (SeekBar) colorDialogView.findViewById(
46 R.id.blueSeekBar);
47 colorView = colorDialogView.findViewById(R.id.colorView);
48
49 // register SeekBar event listeners
50 alphaSeekBar.setOnSeekBarChangeListener(colorChangedListener);
51 redSeekBar.setOnSeekBarChangeListener(colorChangedListener);
52 greenSeekBar.setOnSeekBarChangeListener(colorChangedListener);
53 blueSeekBar.setOnSeekBarChangeListener(colorChangedListener);
54
55 // use current drawing color to set SeekBar values
56 final DoodleView doodleView = getDoodleFragment().getDoodleView();
57 color = doodleView.getDrawingColor();
58 alphaSeekBar.setProgress(Color.alpha(color));
59 redSeekBar.setProgress(Color.red(color));
60 greenSeekBar.setProgress(Color.green(color));
61 blueSeekBar.setProgress(Color.blue(color));
62
63 // add Set Color Button
64 builder.setPositiveButton(R.string.button_set_color,
65 new DialogInterface.OnClickListener() {
66 public void onClick(DialogInterface dialog, int id) {
67 doodleView.setDrawingColor(color);
68 }
69 }
70 );
71
72 return builder.create(); // return dialog
73 }
74
Line 56 (Fig. 5.44) calls method getDoodleFragment
(Fig. 5.45) to get a reference to the DoodleFragment
, then calls the MainActivityFragment
’s getDoodleView
method to get the DoodleView
. Lines 57–61 get the DoodleView
’s current drawing color, then use it to set each SeekBar
’s value. Color
’s static
methods alpha, red, green and blue extract the ARGB values from the color, and SeekBar
’s setProgress
method positions the thumbs. Lines 64–70 configure the AlertDialog
’s positive button to set the DoodleView
’s new drawing color. Line 72 returns the AlertDialog
.
Method getDoodleFragment
(Fig. 5.45) simply uses the FragmentManager
to get a reference to the DoodleFragment
.
75 // gets a reference to the MainActivityFragment
76 private MainActivityFragment getDoodleFragment() {
77 return (MainActivityFragment) getFragmentManager().findFragmentById(
78 R.id.doodleFragment);
79 }
80
When the ColorDialogFragment
is added to a parent Activity
, method onAttach
(Fig. 5.46, lines 82–89) is called. Line 85 gets a reference to the MainActivityFragment
. If that reference is not null
, line 88 calls MainActivityFragment
’s setDialogOnScreen
method to indicate that the Choose Color dialog is now displayed. When the ColorDialogFragment
is removed from a parent Activity
, method onDetach
(lines 92–99) is called. Line 98 calls MainActivityFragment
’s setDialogOnScreen
method to indicate that the Choose Color dialog is no longer on the screen.
81 // tell MainActivityFragment that dialog is now displayed
82 @Override
83 public void onAttach(Activity activity) {
84 super.onAttach(activity);
85 MainActivityFragment fragment = getDoodleFragment();
86
87 if (fragment != null)
88 fragment.setDialogOnScreen(true);
89 }
90
91 // tell MainActivityFragment that dialog is no longer displayed
92 @Override
93 public void onDetach() {
94 super.onDetach();
95 MainActivityFragment fragment = getDoodleFragment();
96
97 if (fragment != null)
98 fragment.setDialogOnScreen(false);
99 }
100
Figure 5.47 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. 5.44 (lines 50–53). Method onProgressChanged
(Fig. 5.47, lines 105–114) is called when the position of a SeekBar
’s thumb changes. If the user moved a SeekBar
’s thumb (line 109), lines 110–112 store the new color. Class Color
’s static
method argb combines the SeekBar
s’ values into a Color
and returns the appropriate color as an int
. We then use class View
’s setBackgroundColor method to update the colorView
with a color that matches the current state of the SeekBar
s.
101 // OnSeekBarChangeListener for the SeekBars in the color dialog
102 private final OnSeekBarChangeListener colorChangedListener =
103 new OnSeekBarChangeListener() {
104 // display the updated color
105 @Override
106 public void onProgressChanged(SeekBar seekBar, int progress,
107 boolean fromUser) {
108
109 if (fromUser) // user, not program, changed SeekBar progress
110 color = Color.argb(alphaSeekBar.getProgress(),
111 redSeekBar.getProgress(), greenSeekBar.getProgress(),
112 blueSeekBar.getProgress());
113 colorView.setBackgroundColor(color);
114 }
115
116 @Override
117 public void onStartTrackingTouch(SeekBar seekBar) {} // required
118
119 @Override
120 public void onStopTrackingTouch(SeekBar seekBar) {} // required
121 };
122 }
Class LineWidthDialogFragment
(Fig. 5.48) extends DialogFragment
to create an AlertDialog
for setting the line width. The class is similar to class ColorDialogFragment
, so we discuss only the key differences here. The class’s only instance variable is an ImageView
(line 21) in which we draw a line showing the current line-width setting.
1 // LineWidthDialogFragment.java
2 // Allows user to set the drawing color on the DoodleView
3 package com.deitel.doodlz;
4
5 import android.app.Activity;
6 import android.app.AlertDialog;
7 import android.app.Dialog;
8 import android.content.DialogInterface;
9 import android.graphics.Bitmap;
10 import android.graphics.Canvas;
11 import android.graphics.Paint;
12 import android.os.Bundle;
13 import android.support.v4.app.DialogFragment;
14 import android.view.View;
15 import android.widget.ImageView;
16 import android.widget.SeekBar;
17 import android.widget.SeekBar.OnSeekBarChangeListener;
18
19 // class for the Select Line Width dialog
20 public class LineWidthDialogFragment extends DialogFragment {
21 private ImageView widthImageView;
22
23 // create an AlertDialog and return it
24 @Override
25 public Dialog onCreateDialog(Bundle bundle) {
26 // create the dialog
27 AlertDialog.Builder builder =
28 new AlertDialog.Builder(getActivity());
29 View lineWidthDialogView =
30 getActivity().getLayoutInflater().inflate(
31 R.layout.fragment_line_width, null);
32 builder.setView(lineWidthDialogView); // add GUI to dialog
33
34 // set the AlertDialog's message
35 builder.setTitle(R.string.title_line_width_dialog);
36
37 // get the ImageView
38 widthImageView = (ImageView) lineWidthDialogView.findViewById(
39 R.id.widthImageView);
40
41 // configure widthSeekBar
42 final DoodleView doodleView = getDoodleFragment().getDoodleView();
43 final SeekBar widthSeekBar = (SeekBar)
44 lineWidthDialogView.findViewById(R.id.widthSeekBar);
45 widthSeekBar.setOnSeekBarChangeListener(lineWidthChanged);
46 widthSeekBar.setProgress(doodleView.getLineWidth());
47
48 // add Set Line Width Button
49 builder.setPositiveButton(R.string.button_set_line_width,
50 new DialogInterface.OnClickListener() {
51 public void onClick(DialogInterface dialog, int id) {
52 doodleView.setLineWidth(widthSeekBar.getProgress());
53 }
54 }
55 );
56
57 return builder.create(); // return dialog
58 }
59
60 // return a reference to the MainActivityFragment
61 private MainActivityFragment getDoodleFragment() {
62 return (MainActivityFragment) getFragmentManager().findFragmentById(
63 R.id.doodleFragment);
64 }
65
66 // tell MainActivityFragment that dialog is now displayed
67 @Override
68 public void onAttach(Activity activity) {
69 super.onAttach(activity);
70 MainActivityFragment fragment = getDoodleFragment();
71
72 if (fragment != null)
73 fragment.setDialogOnScreen(true);
74 }
75
76 // tell MainActivityFragment that dialog is no longer displayed
77 @Override
78 public void onDetach() {
79 super.onDetach();
80 MainActivityFragment fragment = getDoodleFragment();
81
82 if (fragment != null)
83 fragment.setDialogOnScreen(false);
84 }
85
86 // OnSeekBarChangeListener for the SeekBar in the width dialog
87 private final OnSeekBarChangeListener lineWidthChanged =
88 new OnSeekBarChangeListener() {
89 final Bitmap bitmap = Bitmap.createBitmap(
90 400, 100, Bitmap.Config.ARGB_8888);
91 final Canvas canvas = new Canvas(bitmap); // draws into bitmap
92
93 @Override
94 public void onProgressChanged(SeekBar seekBar, int progress,
95 boolean fromUser) {
96 // configure a Paint object for the current SeekBar value
97 Paint p = new Paint();
98 p.setColor(
99 getDoodleFragment().getDoodleView().getDrawingColor());
100 p.setStrokeCap(Paint.Cap.ROUND);
101 p.setStrokeWidth(progress);
102
103 // erase the bitmap and redraw the line
104 bitmap.eraseColor(
105 getResources().getColor(android.R.color.transparent,
106 getContext().getTheme()));
107 canvas.drawLine(30, 50, 370, 50, p);
108 widthImageView.setImageBitmap(bitmap);
109 }
110
111 @Override
112 public void onStartTrackingTouch(SeekBar seekBar) {} // required
113
114 @Override
115 public void onStopTrackingTouch(SeekBar seekBar) {} // required
116 };
117 }
Method onCreateDialog
(lines 24–58) inflates the custom View
(lines 29–31) defined by fragment_line_width.xml
that displays the GUI for selecting the line width, then attaches that View
to the AlertDialog
by calling AlertDialog.Builder
’s setView
method (line 32). Lines 38–39 get a reference to the ImageView
in which the sample line will be drawn. Next, lines 42–46 get a reference to the widthSeekBar
, register lineWidthChanged
(lines 87–116) as the SeekBar
’s listener and set the SeekBar
’s current value to the current line width. Lines 49–55 define the dialog’s positive button to call the DoodleView
’s setLineWidth
method when the user touches the Set Line Width button. Line 57 returns the AlertDialog
for display.
Lines 87–116 define the lineWidthChanged OnSeekBarChangeListener
that responds to events when the user adjusts the SeekBar
in the Choose Line Width dialog. Lines 89–90 create a Bitmap
on which to display a sample line representing the selected line thickness. Line 91 creates a Canvas
for drawing on the Bitmap
. Method onProgressChanged
(lines 93–109) draws the sample line based on the current drawing color and the SeekBar
’s value. First, lines 97–101 configure a Paint
object for drawing the sample line. Class Paint
’s setStrokeCap method (line 100) specifies the appearance of the line ends—in this case, they’re rounded (Paint.Cap.ROUND
). Lines 104–106 clear bitmap
’s background to the predefined Android color android.R.color.transparent
with Bitmap
method eraseColor. We use canvas
to draw the sample line. Finally, line 108 displays bitmap
in the widthImageView
by passing it to ImageView
’s setImageBitmap method.
Class EraseImageDialogFragment
(Fig. 5.49) extends DialogFragment
to create an AlertDialog
that confirms whether the user really wants to erase the entire image. The class is similar to class ColorDialogFragment
and LineWidthDialogFragment
, so we discuss only method onCreateDialog
(lines 15–35) here. The method creates an AlertDialog
with Erase Image and Cancel button. Lines 24–30 configure the Erase Image button as the positive button—when the user touches this, line 27 in the button’s listener calls the DoodleView
’s clear
method to erase the image. Line 33 configures Cancel as the negative button—when the user touches this, the dialog is dismissed. In this case, we use the predefined Android String
resource android.R.string.cancel
. For other predefined String
resources, visit
Line 34 returns the AlertDialog
.
1 // EraseImageDialogFragment.java
2 // Allows user to erase image
3 package com.deitel.doodlz;
4
5 import android.app.Activity;
6 import android.app.AlertDialog;
7 import android.app.Dialog;
8 import android.support.v4.app.DialogFragment;
9 import android.content.DialogInterface;
10 import android.os.Bundle;
11
12 // class for the Erase Image dialog
13 public class EraseImageDialogFragment extends DialogFragment {
14 // create an AlertDialog and return it
15 @Override
16 public Dialog onCreateDialog(Bundle bundle) {
17 AlertDialog.Builder builder =
18 new AlertDialog.Builder(getActivity());
19
20 // set the AlertDialog's message
21 builder.setMessage(R.string.message_erase);
22
23 // add Erase Button
24 builder.setPositiveButton(R.string.button_erase,
25 new DialogInterface.OnClickListener() {
26 public void onClick(DialogInterface dialog, int id) {
27 getDoodleFragment().getDoodleView().clear(); // clear image
28 }
29 }
30 );
31
32 // add cancel Button
33 builder.setNegativeButton(android.R.string.cancel, null);
34 return builder.create(); // return dialog
35 }
36
37 // gets a reference to the MainActivityFragment
38 private MainActivityFragment getDoodleFragment() {
39 return (MainActivityFragment) getFragmentManager().findFragmentById(
40 R.id.doodleFragment);
41 }
42
43 // tell MainActivityFragment that dialog is now displayed
44 @Override
45 public void onAttach(Activity activity) {
46 super.onAttach(activity);
47 MainActivityFragment fragment = getDoodleFragment();
48
49 if (fragment != null)
50 fragment.setDialogOnScreen(true);
51 }
52
53 // tell MainActivityFragment that dialog is no longer displayed
54 @Override
55 public void onDetach() {
56 super.onDetach();
57 MainActivityFragment fragment = getDoodleFragment();
58
59 if (fragment != null)
60 fragment.setDialogOnScreen(false);
61 }
62 }
In this chapter, you built the Doodlz app, which enables users to paint by dragging one or more fingers across the screen. You implemented a shake-to-erase feature by using Android’s SensorManager
to register a SensorEventListener
that responds to accelerometer events, and you learned that Android supports many other sensors.
You created subclasses of DialogFragment
for displaying custom View
s in AlertDialog
s. You also overrode the Fragment
lifecycle methods onAttach
and onDetach
, which are called when a Fragment
is attached to or detached from a parent Activity
, respectively.
We showed how to associate a Canvas
with a Bitmap
, then use the Canvas
to draw into the Bitmap
. We demonstrated how to handle multitouch events, so the app could respond to multiple fingers being dragged across the screen at the same time. You stored the information for each individual finger as a Path
. You processed the touch events by overriding the View
method onTouchEvent
, which receives a MotionEvent
containing the event type and the ID of the pointer (finger) that generated the event. We used the IDs to distinguish among the fingers and add information to the corresponding Path
objects.
You used a ContentResolver
and the MediaStore.Images.Media.insertImage
method to save an image onto the device. To enable this feature, you used Android 6.0’s new permissions model to request permission from the user to save to external storage.
We showed how to use the printing framework to allow users to print their drawings. You used the Android Support Library’s PrintHelper
class to print a Bitmap
. The PrintHelper
displayed a user interface for selecting a printer or saving the image into a PDF document. To incorporate Android Support Library features into the app, you used Gradle to specify the app’s dependency on features from that library.
In Chapter 6, you’ll create a Cannon Game using multithreading and frame-by-frame animation. You’ll handle touch gestures to fire a cannon. You’ll also learn how to create a game loop that updates the display as fast as possible to create smooth animations and to make the game feel like it executes at the same speed regardless of a given device’s processor speed.
5.1 Fill in the blanks in each of the following statements:
a) 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.
b) A Path
object (package android.graphics
) represents a geometric path consisting of line segments and _____________.
c) You use the type of the touch event to determine whether the user has touched the screen, _____________ or lifted a finger from the screen.
d) Use class SensorManager
’s _____________ method to stop listening for accelerometer events.
e) Override SensorEventListener
method _____________ to process accelerometer events.
f) Override Fragment
method _____________ to respond to the event when a Fragment
is attached to a parent Activity
.
g) When a View
needs to be redrawn, its _____________ method is called.
h) 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.
5.2 State whether each of the following is true or false. If false, explain why.
a) You unregister the accelerometer event handler when the app is sent to the foreground.
b) Call the inherited View
method validate
to indicate that the View
needs to be redrawn.
c) If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
, the user touched the screen with the same finger.
d) 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.
e) Method MediaStore.Images.Media.saveImage
saves a Bitmap
into the Photos app.
a) SensorEventListener
.
b) curves.
c) dragged across the screen.
d) unregisterListener
.
e) onSensorChanged
.
f) onAttach
.
g) onDraw
.
h) getActionMasked
.
a) False. You unregister the accelerometer event handler when the app is sent to the background.
b) False. Call the inherited View
method invalidate
to indicate that the View
needs to be redrawn.
c) False. If the action is MotionEvent.ACTION_DOWN
or MotionEvent.ACTION_POINTER_DOWN
, the user touched the screen with a new finger.
d) 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.
e) False. The method MediaStore.Images.Media.insertImage
saves a Bitmap
into the device’s Photos app.
a) Most Android devices have a(n) _____________ that allows apps to detect movement.
b) Override Fragment
method _____________ to respond to the event when a Fragment
is removed from a parent Activity
.
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 with SensorManager
method registerListener
, which receives: the SensorEventListener
that responds 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) Paint
method _____________ sets the stroke width to the specified number of pixels.
h) Android supports _____________—that is, having multiple fingers touching the screen.
i) The Android Support Library’s _____________ class provides a GUI for selecting a printer and method for printing a Bitmap
.
j) Android Studio uses the _____________ to compile your code into an APK file and to handle project dependencies, such as including in the build process any libraries used by the app.
k) The attribute android:showAsAction
defines how a menu item should appear on the app bar. The value _____________ specifies that this item should be visible on the app bar if there’s room to display the item.
5.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 meters/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) Prior to Android 6.0, the user was asked to grant some app permissions at installation time and others at execution time.
h) With the new Android 6.0 permissions model, an app is installed without asking the user for any permissions before installation. The user is asked to grant a permission only the first time the corresponding feature is used.
5.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. Erasing 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 the line thickness. Class MotionEvent
has methods that allow you to get the pressure of the touch (http://developer.android.com/reference/android/view/MotionEvent.html
).
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.
5.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.
5.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.
5.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.
5.9 (Enhanced Block Breaker Game) Modify the Block Breaker game in Exercise 5.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.
5.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.
5.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.
5.12 (Kaleidascope App) Create an app that simulates a kaleidoscope. Allow the user to shake the device to redraw the screen.
5.13 (Game of Snake App) Research the Game of Snake online and develop an app that allows a user to play the game.