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