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