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 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 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 120 public boolean onPrepareOptionsMenu(Menu menu) { 121 menu.findItem(FADE_ID).setChecked(mFading); 122 return super.onPrepareOptionsMenu(menu); 123 } 124 125 @Override 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 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 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 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 */ 178 void startFading() { 179 mHandler.removeMessages(MSG_FADE); 180 scheduleFade(); 181 } 182 183 /** 184 * Stop the pulse to fade the screen. 185 */ 186 void stopFading() { 187 mHandler.removeMessages(MSG_FADE); 188 } 189 190 /** 191 * Schedule a fade message for later. 192 */ 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 246 public PaintView(Context c) { 247 super(c); 248 init(); 249 } 250 251 public PaintView(Context c, AttributeSet attrs) { 252 super(c, attrs); 253 init(); 254 } 255 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 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 275 public void fade() { 276 if (mCanvas != null && mFadeSteps < MAX_FADE_STEPS) { 277 mCanvas.drawPaint(mFadePaint); 278 invalidate(); 279 280 mFadeSteps++; 281 } 282 } 283 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 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 332 protected void onDraw(Canvas canvas) { 333 if (mBitmap != null) { 334 canvas.drawBitmap(mBitmap, 0, 0, null); 335 } 336 } 337 338 @Override 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 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 369 public boolean onTouchEvent(MotionEvent event) { 370 return onTouchOrHoverEvent(event, true /*isTouch*/); 371 } 372 373 @Override 374 public boolean onHoverEvent(MotionEvent event) { 375 return onTouchOrHoverEvent(event, false /*isTouch*/); 376 } 377 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 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 443 private void advanceColor() { 444 mColorIndex = (mColorIndex + 1) % COLORS.length; 445 } 446 447 private void paint(PaintMode mode, float x, float y) { 448 paint(mode, x, y, 1.0f, 0, 0, 0, 0, 0); 449 } 450 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(); 491 private void drawOval(Canvas canvas, float x, float y, float major, float minor, 492 float orientation, Paint paint) { 493 canvas.save(Canvas.MATRIX_SAVE_FLAG); 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 */ 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