5.8 DoodleView Class

The DoodleView class (Sections 5.8.15.8.12) processes the user’s touches and draws the corresponding lines.

5.8.1 package Statement and import Statements

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


Fig. 5.31 | DooldleView package statement and import statements.

5.8.2 static and Instance Variables

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


Fig. 5.32 | DoodleView static and instance variables.

5.8.3 Constructor

The constructor (Fig. 5.33) initializes several of the class’s instance variables—the two Maps 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


Fig. 5.33 | DoodleView constructor.

5.8.4 Overridden View Method onSizeChanged

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.


Image 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


Fig. 5.34 | Overridden View method onSizeChanged.

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.

5.8.5 Methods clear, setDrawingColor, getDrawingColor, setLineWidth and getLineWidth

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


Fig. 5.35 | DoodleView methods clear, setDrawingColor, getDrawingColor, setLineWidth and getLineWidth.

5.8.6 Overridden View Method onDraw

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


Fig. 5.36 | Overridden View method onDraw.

5.8.7 Overridden View Method onTouchEvent

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


Fig. 5.37 | Overridden View method onTouchEvent.

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

5.8.8 touchStarted Method

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


Fig. 5.38 | touchStarted method of class DoodleView.

5.8.9 touchMoved Method

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


Fig. 5.39 | touchMoved method of class DoodleView.

5.8.10 touchEnded Method

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


Fig. 5.40 | touchEnded method of class DoodleView.

5.8.11 Method saveImage

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

• the name of the image

• 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


Fig. 5.41 | DoodleView method saveImage.

5.8.12 Method printImage

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


Fig. 5.42 | DoodleView method printImage.

5.9 ColorDialogFragment Class

Class ColorDialogFragment (Figs. 5.435.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


Fig. 5.43 | ColorDialogFragment’s package statement, import statements and instance variables.

5.9.1 Overridden DialogFragment Method onCreateDialog

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 SeekBars and colorView. Next, lines 50–53 register colorChangedListener (Fig. 5.47) as the listener for the SeekBars’ 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


Fig. 5.44 | Overridden DialogFragment method onCreateDialog.

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.

5.9.2 Method getDoodleFragment

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


Fig. 5.45 | Method getDoodleFragment.

5.9.3 Overridden Fragment Lifecycle Methods onAttach and onDetach

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


Fig. 5.46 | Overridden Fragment lifecycle methods onAttach and onDetach.

5.9.4 Anonymous Inner Class That Responds to the Events of the Alpha, Red, Green and Blue SeekBars

Figure 5.47 defines an anonymous inner class that implements interface OnSeekBarChangeListener to respond to events when the user adjusts the SeekBars in the Choose Color Dialog. This was registered as the SeekBars’ 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 SeekBars’ 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 SeekBars.


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  }


Fig. 5.47 | Anonymous inner class that implements interface OnSeekBarChangeListener to respond to the events of the alpha, red, green and blue SeekBars.

5.10 LineWidthDialogFragment Class

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   }


Fig. 5.48 | Class LineWidthDialogFragment.

5.10.1 Method onCreateDialog

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.

5.10.2 Anonymous Inner Class That Responds to the Events of the widthSeekBar

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.

5.11 EraseImageDialogFragment Class

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   }


Fig. 5.49 | Class EraseImageDialogFragment.

5.12 Wrap-Up

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 Views in AlertDialogs. 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.

Self-Review Exercises

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.

Answers to Self-Review Exercises

5.1

a) SensorEventListener.

b) curves.

c) dragged across the screen.

d) unregisterListener.

e) onSensorChanged.

f) onAttach.

g) onDraw.

h) getActionMasked.

5.2

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.

Exercises

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

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

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