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 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 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 122 public boolean onPrepareOptionsMenu(Menu menu) { 123 menu.findItem(FADE_ID).setChecked(mFading); 124 return super.onPrepareOptionsMenu(menu); 125 } 126 127 @Override 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 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 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 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 */ 180 void startFading() { 181 mHandler.removeMessages(MSG_FADE); 182 scheduleFade(); 183 } 184 185 /** 186 * Stop the pulse to fade the screen. 187 */ 188 void stopFading() { 189 mHandler.removeMessages(MSG_FADE); 190 } 191 192 /** 193 * Schedule a fade message for later. 194 */ 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 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 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 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 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 299 protected void onDraw(Canvas canvas) { 300 if (mBitmap != null) { 301 canvas.drawBitmap(mBitmap, 0, 0, null); 302 } 303 } 304 305 @Override 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 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 336 public boolean onTouchEvent(MotionEvent event) { 337 return onTouchOrHoverEvent(event, true /*isTouch*/); 338 } 339 340 @Override 341 public boolean onHoverEvent(MotionEvent event) { 342 return onTouchOrHoverEvent(event, false /*isTouch*/); 343 } 344 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 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 410 private void advanceColor() { 411 mColorIndex = (mColorIndex + 1) % COLORS.length; 412 } 413 414 private void paint(PaintMode mode, float x, float y) { 415 paint(mode, x, y, 1.0f, 0, 0, 0, 0, 0); 416 } 417 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(); 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 */ 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