1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.apis.graphics;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.util.AttributeSet;
30 import android.view.Menu;
31 import android.view.MenuItem;
32 import android.view.MotionEvent;
33 import android.view.View;
34 
35 import java.util.Random;
36 
37 /**
38  * Demonstrates the handling of touch screen, stylus, mouse and trackball events to
39  * implement a simple painting app.
40  * <p>
41  * Drawing with a touch screen is accomplished by drawing a point at the
42  * location of the touch.  When pressure information is available, it is used
43  * to change the intensity of the color.  When size and orientation information
44  * is available, it is used to directly adjust the size and orientation of the
45  * brush.
46  * </p><p>
47  * Drawing with a stylus is similar to drawing with a touch screen, with a
48  * few added refinements.  First, there may be multiple tools available including
49  * an eraser tool.  Second, the tilt angle and orientation of the stylus can be
50  * used to control the direction of paint.  Third, the stylus buttons can be used
51  * to perform various actions.  Here we use one button to cycle colors and the
52  * other to airbrush from a distance.
53  * </p><p>
54  * Drawing with a mouse is similar to drawing with a touch screen, but as with
55  * a stylus we have extra buttons.  Here we use the primary button to draw,
56  * the secondary button to cycle colors and the tertiary button to airbrush.
57  * </p><p>
58  * Drawing with a trackball is a simple matter of using the relative motions
59  * of the trackball to move the paint brush around.  The trackball may also
60  * have a button, which we use to cycle through colors.
61  * </p>
62  */
63 public class TouchPaint extends GraphicsActivity {
64     /** Used as a pulse to gradually fade the contents of the window. */
65     private static final int MSG_FADE = 1;
66 
67     /** Menu ID for the command to clear the window. */
68     private static final int CLEAR_ID = Menu.FIRST;
69 
70     /** Menu ID for the command to toggle fading. */
71     private static final int FADE_ID = Menu.FIRST+1;
72 
73     /** How often to fade the contents of the window (in ms). */
74     private static final int FADE_DELAY = 100;
75 
76     /** Colors to cycle through. */
77     static final int[] COLORS = new int[] {
78         Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
79         Color.CYAN, Color.BLUE, Color.MAGENTA,
80     };
81 
82     /** Background color. */
83     static final int BACKGROUND_COLOR = Color.BLACK;
84 
85     /** The view responsible for drawing the window. */
86     PaintView mView;
87 
88     /** Is fading mode enabled? */
89     boolean mFading;
90 
91     @Override
onCreate(Bundle savedInstanceState)92     protected void onCreate(Bundle savedInstanceState) {
93         super.onCreate(savedInstanceState);
94 
95         // Create and attach the view that is responsible for painting.
96         mView = new PaintView(this);
97         setContentView(mView);
98         mView.requestFocus();
99 
100         // Restore the fading option if we are being thawed from a
101         // previously saved state.  Note that we are not currently remembering
102         // the contents of the bitmap.
103         if (savedInstanceState != null) {
104             mFading = savedInstanceState.getBoolean("fading", true);
105             mView.mColorIndex = savedInstanceState.getInt("color", 0);
106         } else {
107             mFading = true;
108             mView.mColorIndex = 0;
109         }
110     }
111 
112     @Override
onCreateOptionsMenu(Menu menu)113     public boolean onCreateOptionsMenu(Menu menu) {
114         menu.add(0, CLEAR_ID, 0, "Clear");
115         menu.add(0, FADE_ID, 0, "Fade").setCheckable(true);
116         return super.onCreateOptionsMenu(menu);
117     }
118 
119     @Override
onPrepareOptionsMenu(Menu menu)120     public boolean onPrepareOptionsMenu(Menu menu) {
121         menu.findItem(FADE_ID).setChecked(mFading);
122         return super.onPrepareOptionsMenu(menu);
123     }
124 
125     @Override
onOptionsItemSelected(MenuItem item)126     public boolean onOptionsItemSelected(MenuItem item) {
127         switch (item.getItemId()) {
128             case CLEAR_ID:
129                 mView.clear();
130                 return true;
131             case FADE_ID:
132                 mFading = !mFading;
133                 if (mFading) {
134                     startFading();
135                 } else {
136                     stopFading();
137                 }
138                 return true;
139             default:
140                 return super.onOptionsItemSelected(item);
141         }
142     }
143 
144     @Override
onResume()145     protected void onResume() {
146         super.onResume();
147 
148         // If fading mode is enabled, then as long as we are resumed we want
149         // to run pulse to fade the contents.
150         if (mFading) {
151             startFading();
152         }
153     }
154 
155     @Override
onSaveInstanceState(Bundle outState)156     protected void onSaveInstanceState(Bundle outState) {
157         super.onSaveInstanceState(outState);
158 
159         // Save away the fading state to restore if needed later.  Note that
160         // we do not currently save the contents of the display.
161         outState.putBoolean("fading", mFading);
162         outState.putInt("color", mView.mColorIndex);
163     }
164 
165     @Override
onPause()166     protected void onPause() {
167         super.onPause();
168 
169         // Make sure to never run the fading pulse while we are paused or
170         // stopped.
171         stopFading();
172     }
173 
174     /**
175      * Start up the pulse to fade the screen, clearing any existing pulse to
176      * ensure that we don't have multiple pulses running at a time.
177      */
startFading()178     void startFading() {
179         mHandler.removeMessages(MSG_FADE);
180         scheduleFade();
181     }
182 
183     /**
184      * Stop the pulse to fade the screen.
185      */
stopFading()186     void stopFading() {
187         mHandler.removeMessages(MSG_FADE);
188     }
189 
190     /**
191      * Schedule a fade message for later.
192      */
scheduleFade()193     void scheduleFade() {
194         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FADE), FADE_DELAY);
195     }
196 
197     private Handler mHandler = new Handler() {
198         @Override
199         public void handleMessage(Message msg) {
200             switch (msg.what) {
201                 // Upon receiving the fade pulse, we have the view perform a
202                 // fade and then enqueue a new message to pulse at the desired
203                 // next time.
204                 case MSG_FADE: {
205                     mView.fade();
206                     scheduleFade();
207                     break;
208                 }
209                 default:
210                     super.handleMessage(msg);
211             }
212         }
213     };
214 
215     enum PaintMode {
216         Draw,
217         Splat,
218         Erase,
219     }
220 
221     /**
222      * This view implements the drawing canvas.
223      *
224      * It handles all of the input events and drawing functions.
225      */
226     public static class PaintView extends View {
227         private static final int FADE_ALPHA = 0x06;
228         private static final int MAX_FADE_STEPS = 256 / (FADE_ALPHA/2) + 4;
229         private static final int TRACKBALL_SCALE = 10;
230 
231         private static final int SPLAT_VECTORS = 40;
232 
233         private final Random mRandom = new Random();
234         private Bitmap mBitmap;
235         private Canvas mCanvas;
236         private final Paint mPaint = new Paint();
237         private final Paint mFadePaint = new Paint();
238         private float mCurX;
239         private float mCurY;
240         private int mOldButtonState;
241         private int mFadeSteps = MAX_FADE_STEPS;
242 
243         /** The index of the current color to use. */
244         int mColorIndex;
245 
PaintView(Context c)246         public PaintView(Context c) {
247             super(c);
248             init();
249         }
250 
PaintView(Context c, AttributeSet attrs)251         public PaintView(Context c, AttributeSet attrs) {
252             super(c, attrs);
253             init();
254         }
255 
init()256         private void init() {
257             setFocusable(true);
258 
259             mPaint.setAntiAlias(true);
260 
261             mFadePaint.setColor(BACKGROUND_COLOR);
262             mFadePaint.setAlpha(FADE_ALPHA);
263         }
264 
clear()265         public void clear() {
266             if (mCanvas != null) {
267                 mPaint.setColor(BACKGROUND_COLOR);
268                 mCanvas.drawPaint(mPaint);
269                 invalidate();
270 
271                 mFadeSteps = MAX_FADE_STEPS;
272             }
273         }
274 
fade()275         public void fade() {
276             if (mCanvas != null && mFadeSteps < MAX_FADE_STEPS) {
277                 mCanvas.drawPaint(mFadePaint);
278                 invalidate();
279 
280                 mFadeSteps++;
281             }
282         }
283 
text(String text)284         public void text(String text) {
285             if (mBitmap != null) {
286                 final int width = mBitmap.getWidth();
287                 final int height = mBitmap.getHeight();
288                 mPaint.setColor(COLORS[mColorIndex]);
289                 mPaint.setAlpha(255);
290                 int size = height;
291                 mPaint.setTextSize(size);
292                 Rect bounds = new Rect();
293                 mPaint.getTextBounds(text, 0, text.length(), bounds);
294                 int twidth = bounds.width();
295                 twidth += (twidth/4);
296                 if (twidth > width) {
297                     size = (size*width)/twidth;
298                     mPaint.setTextSize(size);
299                     mPaint.getTextBounds(text, 0, text.length(), bounds);
300                 }
301                 Paint.FontMetrics fm = mPaint.getFontMetrics();
302                 mCanvas.drawText(text, (width-bounds.width())/2,
303                         ((height-size)/2) - fm.ascent, mPaint);
304                 mFadeSteps = 0;
305                 invalidate();
306             }
307         }
308 
309         @Override
onSizeChanged(int w, int h, int oldw, int oldh)310         protected void onSizeChanged(int w, int h, int oldw, int oldh) {
311             int curW = mBitmap != null ? mBitmap.getWidth() : 0;
312             int curH = mBitmap != null ? mBitmap.getHeight() : 0;
313             if (curW >= w && curH >= h) {
314                 return;
315             }
316 
317             if (curW < w) curW = w;
318             if (curH < h) curH = h;
319 
320             Bitmap newBitmap = Bitmap.createBitmap(curW, curH, Bitmap.Config.ARGB_8888);
321             Canvas newCanvas = new Canvas();
322             newCanvas.setBitmap(newBitmap);
323             if (mBitmap != null) {
324                 newCanvas.drawBitmap(mBitmap, 0, 0, null);
325             }
326             mBitmap = newBitmap;
327             mCanvas = newCanvas;
328             mFadeSteps = MAX_FADE_STEPS;
329         }
330 
331         @Override
onDraw(Canvas canvas)332         protected void onDraw(Canvas canvas) {
333             if (mBitmap != null) {
334                 canvas.drawBitmap(mBitmap, 0, 0, null);
335             }
336         }
337 
338         @Override
onTrackballEvent(MotionEvent event)339         public boolean onTrackballEvent(MotionEvent event) {
340             final int action = event.getActionMasked();
341             if (action == MotionEvent.ACTION_DOWN) {
342                 // Advance color when the trackball button is pressed.
343                 advanceColor();
344             }
345 
346             if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
347                 final int N = event.getHistorySize();
348                 final float scaleX = event.getXPrecision() * TRACKBALL_SCALE;
349                 final float scaleY = event.getYPrecision() * TRACKBALL_SCALE;
350                 for (int i = 0; i < N; i++) {
351                     moveTrackball(event.getHistoricalX(i) * scaleX,
352                             event.getHistoricalY(i) * scaleY);
353                 }
354                 moveTrackball(event.getX() * scaleX, event.getY() * scaleY);
355             }
356             return true;
357         }
358 
moveTrackball(float deltaX, float deltaY)359         private void moveTrackball(float deltaX, float deltaY) {
360             final int curW = mBitmap != null ? mBitmap.getWidth() : 0;
361             final int curH = mBitmap != null ? mBitmap.getHeight() : 0;
362 
363             mCurX = Math.max(Math.min(mCurX + deltaX, curW - 1), 0);
364             mCurY = Math.max(Math.min(mCurY + deltaY, curH - 1), 0);
365             paint(PaintMode.Draw, mCurX, mCurY);
366         }
367 
368         @Override
onTouchEvent(MotionEvent event)369         public boolean onTouchEvent(MotionEvent event) {
370             return onTouchOrHoverEvent(event, true /*isTouch*/);
371         }
372 
373         @Override
onHoverEvent(MotionEvent event)374         public boolean onHoverEvent(MotionEvent event) {
375             return onTouchOrHoverEvent(event, false /*isTouch*/);
376         }
377 
onTouchOrHoverEvent(MotionEvent event, boolean isTouch)378         private boolean onTouchOrHoverEvent(MotionEvent event, boolean isTouch) {
379             final int buttonState = event.getButtonState();
380             int pressedButtons = buttonState & ~mOldButtonState;
381             mOldButtonState = buttonState;
382 
383             if ((pressedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
384                 // Advance color when the right mouse button or first stylus button
385                 // is pressed.
386                 advanceColor();
387             }
388 
389             PaintMode mode;
390             if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0) {
391                 // Splat paint when the middle mouse button or second stylus button is pressed.
392                 mode = PaintMode.Splat;
393             } else if (isTouch || (buttonState & MotionEvent.BUTTON_PRIMARY) != 0) {
394                 // Draw paint when touching or if the primary button is pressed.
395                 mode = PaintMode.Draw;
396             } else {
397                 // Otherwise, do not paint anything.
398                 return false;
399             }
400 
401             final int action = event.getActionMasked();
402             if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE
403                     || action == MotionEvent.ACTION_HOVER_MOVE) {
404                 final int N = event.getHistorySize();
405                 final int P = event.getPointerCount();
406                 for (int i = 0; i < N; i++) {
407                     for (int j = 0; j < P; j++) {
408                         paint(getPaintModeForTool(event.getToolType(j), mode),
409                                 event.getHistoricalX(j, i),
410                                 event.getHistoricalY(j, i),
411                                 event.getHistoricalPressure(j, i),
412                                 event.getHistoricalTouchMajor(j, i),
413                                 event.getHistoricalTouchMinor(j, i),
414                                 event.getHistoricalOrientation(j, i),
415                                 event.getHistoricalAxisValue(MotionEvent.AXIS_DISTANCE, j, i),
416                                 event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, j, i));
417                     }
418                 }
419                 for (int j = 0; j < P; j++) {
420                     paint(getPaintModeForTool(event.getToolType(j), mode),
421                             event.getX(j),
422                             event.getY(j),
423                             event.getPressure(j),
424                             event.getTouchMajor(j),
425                             event.getTouchMinor(j),
426                             event.getOrientation(j),
427                             event.getAxisValue(MotionEvent.AXIS_DISTANCE, j),
428                             event.getAxisValue(MotionEvent.AXIS_TILT, j));
429                 }
430                 mCurX = event.getX();
431                 mCurY = event.getY();
432             }
433             return true;
434         }
435 
getPaintModeForTool(int toolType, PaintMode defaultMode)436         private PaintMode getPaintModeForTool(int toolType, PaintMode defaultMode) {
437             if (toolType == MotionEvent.TOOL_TYPE_ERASER) {
438                 return PaintMode.Erase;
439             }
440             return defaultMode;
441         }
442 
advanceColor()443         private void advanceColor() {
444             mColorIndex = (mColorIndex + 1) % COLORS.length;
445         }
446 
paint(PaintMode mode, float x, float y)447         private void paint(PaintMode mode, float x, float y) {
448             paint(mode, x, y, 1.0f, 0, 0, 0, 0, 0);
449         }
450 
paint(PaintMode mode, float x, float y, float pressure, float major, float minor, float orientation, float distance, float tilt)451         private void paint(PaintMode mode, float x, float y, float pressure,
452                 float major, float minor, float orientation,
453                 float distance, float tilt) {
454             if (mBitmap != null) {
455                 if (major <= 0 || minor <= 0) {
456                     // If size is not available, use a default value.
457                     major = minor = 16;
458                 }
459 
460                 switch (mode) {
461                     case Draw:
462                         mPaint.setColor(COLORS[mColorIndex]);
463                         mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
464                         drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
465                         break;
466 
467                     case Erase:
468                         mPaint.setColor(BACKGROUND_COLOR);
469                         mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
470                         drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
471                         break;
472 
473                     case Splat:
474                         mPaint.setColor(COLORS[mColorIndex]);
475                         mPaint.setAlpha(64);
476                         drawSplat(mCanvas, x, y, orientation, distance, tilt, mPaint);
477                         break;
478                 }
479             }
480             mFadeSteps = 0;
481             invalidate();
482         }
483 
484         /**
485          * Draw an oval.
486          *
487          * When the orienation is 0 radians, orients the major axis vertically,
488          * angles less than or greater than 0 radians rotate the major axis left or right.
489          */
490         private final RectF mReusableOvalRect = new RectF();
drawOval(Canvas canvas, float x, float y, float major, float minor, float orientation, Paint paint)491         private void drawOval(Canvas canvas, float x, float y, float major, float minor,
492                 float orientation, Paint paint) {
493             canvas.save();
494             canvas.rotate((float) (orientation * 180 / Math.PI), x, y);
495             mReusableOvalRect.left = x - minor / 2;
496             mReusableOvalRect.right = x + minor / 2;
497             mReusableOvalRect.top = y - major / 2;
498             mReusableOvalRect.bottom = y + major / 2;
499             canvas.drawOval(mReusableOvalRect, paint);
500             canvas.restore();
501         }
502 
503         /**
504          * Splatter paint in an area.
505          *
506          * Chooses random vectors describing the flow of paint from a round nozzle
507          * across a range of a few degrees.  Then adds this vector to the direction
508          * indicated by the orientation and tilt of the tool and throws paint at
509          * the canvas along that vector.
510          *
511          * Repeats the process until a masterpiece is born.
512          */
drawSplat(Canvas canvas, float x, float y, float orientation, float distance, float tilt, Paint paint)513         private void drawSplat(Canvas canvas, float x, float y, float orientation,
514                 float distance, float tilt, Paint paint) {
515             float z = distance * 2 + 10;
516 
517             // Calculate the center of the spray.
518             float nx = (float) (Math.sin(orientation) * Math.sin(tilt));
519             float ny = (float) (- Math.cos(orientation) * Math.sin(tilt));
520             float nz = (float) Math.cos(tilt);
521             if (nz < 0.05) {
522                 return;
523             }
524             float cd = z / nz;
525             float cx = nx * cd;
526             float cy = ny * cd;
527 
528             for (int i = 0; i < SPLAT_VECTORS; i++) {
529                 // Make a random 2D vector that describes the direction of a speck of paint
530                 // ejected by the nozzle in the nozzle's plane, assuming the tool is
531                 // perpendicular to the surface.
532                 double direction = mRandom.nextDouble() * Math.PI * 2;
533                 double dispersion = mRandom.nextGaussian() * 0.2;
534                 double vx = Math.cos(direction) * dispersion;
535                 double vy = Math.sin(direction) * dispersion;
536                 double vz = 1;
537 
538                 // Apply the nozzle tilt angle.
539                 double temp = vy;
540                 vy = temp * Math.cos(tilt) - vz * Math.sin(tilt);
541                 vz = temp * Math.sin(tilt) + vz * Math.cos(tilt);
542 
543                 // Apply the nozzle orientation angle.
544                 temp = vx;
545                 vx = temp * Math.cos(orientation) - vy * Math.sin(orientation);
546                 vy = temp * Math.sin(orientation) + vy * Math.cos(orientation);
547 
548                 // Determine where the paint will hit the surface.
549                 if (vz < 0.05) {
550                     continue;
551                 }
552                 float pd = (float) (z / vz);
553                 float px = (float) (vx * pd);
554                 float py = (float) (vy * pd);
555 
556                 // Throw some paint at this location, relative to the center of the spray.
557                 mCanvas.drawCircle(x + px - cx, y + py - cy, 1.0f, paint);
558             }
559         }
560     }
561 }
562