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