GestureOverlayView.java revision 8d78756c160bda736cccef9ca1a6e2d6a159ac42
1/* 2 * Copyright (C) 2009 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 17package android.gesture; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.Paint; 23import android.graphics.Path; 24import android.graphics.Rect; 25import android.graphics.RectF; 26import android.util.AttributeSet; 27import android.view.MotionEvent; 28import android.view.animation.AnimationUtils; 29import android.view.animation.AccelerateDecelerateInterpolator; 30import android.widget.FrameLayout; 31import android.os.SystemClock; 32import com.android.internal.R; 33 34import java.util.ArrayList; 35 36/** 37 * A transparent overlay for gesture input that can be placed on top of other 38 * widgets or contain other widgets. 39 * 40 * @attr ref android.R.styleable#GestureOverlayView_eventsInterceptionEnabled 41 * @attr ref android.R.styleable#GestureOverlayView_fadeDuration 42 * @attr ref android.R.styleable#GestureOverlayView_fadeOffset 43 * @attr ref android.R.styleable#GestureOverlayView_fadeEnabled 44 * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeWidth 45 * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeAngleThreshold 46 * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeLengthThreshold 47 * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeSquarenessThreshold 48 * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeType 49 * @attr ref android.R.styleable#GestureOverlayView_gestureColor 50 * @attr ref android.R.styleable#GestureOverlayView_orientation 51 * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor 52 */ 53public class GestureOverlayView extends FrameLayout { 54 public static final int GESTURE_STROKE_TYPE_SINGLE = 0; 55 public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1; 56 57 public static final int ORIENTATION_HORIZONTAL = 0; 58 public static final int ORIENTATION_VERTICAL = 1; 59 60 private static final int FADE_ANIMATION_RATE = 16; 61 private static final boolean GESTURE_RENDERING_ANTIALIAS = true; 62 private static final boolean DITHER_FLAG = true; 63 64 private final Paint mGesturePaint = new Paint(); 65 66 private long mFadeDuration = 150; 67 private long mFadeOffset = 420; 68 private long mFadingStart; 69 private boolean mFadingHasStarted; 70 private boolean mFadeEnabled = true; 71 72 private int mCurrentColor; 73 private int mCertainGestureColor = 0xFFFFFF00; 74 private int mUncertainGestureColor = 0x48FFFF00; 75 private float mGestureStrokeWidth = 12.0f; 76 private int mInvalidateExtraBorder = 10; 77 78 private int mGestureStrokeType = GESTURE_STROKE_TYPE_SINGLE; 79 private float mGestureStrokeLengthThreshold = 50.0f; 80 private float mGestureStrokeSquarenessTreshold = 0.275f; 81 private float mGestureStrokeAngleThreshold = 40.0f; 82 83 private int mOrientation = ORIENTATION_VERTICAL; 84 85 private final Rect mInvalidRect = new Rect(); 86 private final Path mPath = new Path(); 87 88 private float mX; 89 private float mY; 90 91 private float mCurveEndX; 92 private float mCurveEndY; 93 94 private float mTotalLength; 95 private boolean mIsGesturing = false; 96 private boolean mInterceptEvents = true; 97 private boolean mIsListeningForGestures; 98 99 // current gesture 100 private Gesture mCurrentGesture; 101 private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); 102 103 // TODO: Make this a list of WeakReferences 104 private final ArrayList<OnGestureListener> mOnGestureListeners = 105 new ArrayList<OnGestureListener>(); 106 // TODO: Make this a list of WeakReferences 107 private final ArrayList<OnGesturePerformedListener> mOnGesturePerformedListeners = 108 new ArrayList<OnGesturePerformedListener>(); 109 110 private boolean mHandleGestureActions; 111 112 // fading out effect 113 private boolean mIsFadingOut = false; 114 private float mFadingAlpha = 1.0f; 115 private final AccelerateDecelerateInterpolator mInterpolator = 116 new AccelerateDecelerateInterpolator(); 117 118 private final FadeOutRunnable mFadingOut = new FadeOutRunnable(); 119 120 public GestureOverlayView(Context context) { 121 super(context); 122 init(); 123 } 124 125 public GestureOverlayView(Context context, AttributeSet attrs) { 126 this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle); 127 } 128 129 public GestureOverlayView(Context context, AttributeSet attrs, int defStyle) { 130 super(context, attrs, defStyle); 131 132 TypedArray a = context.obtainStyledAttributes(attrs, 133 R.styleable.GestureOverlayView, defStyle, 0); 134 135 mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth, 136 mGestureStrokeWidth); 137 mInvalidateExtraBorder = Math.max(1, ((int) mGestureStrokeWidth) - 1); 138 mCertainGestureColor = a.getColor(R.styleable.GestureOverlayView_gestureColor, 139 mCertainGestureColor); 140 mUncertainGestureColor = a.getColor(R.styleable.GestureOverlayView_uncertainGestureColor, 141 mUncertainGestureColor); 142 mFadeDuration = a.getInt(R.styleable.GestureOverlayView_fadeDuration, (int) mFadeDuration); 143 mFadeOffset = a.getInt(R.styleable.GestureOverlayView_fadeOffset, (int) mFadeOffset); 144 mGestureStrokeType = a.getInt(R.styleable.GestureOverlayView_gestureStrokeType, 145 mGestureStrokeType); 146 mGestureStrokeLengthThreshold = a.getFloat( 147 R.styleable.GestureOverlayView_gestureStrokeLengthThreshold, 148 mGestureStrokeLengthThreshold); 149 mGestureStrokeAngleThreshold = a.getFloat( 150 R.styleable.GestureOverlayView_gestureStrokeAngleThreshold, 151 mGestureStrokeAngleThreshold); 152 mGestureStrokeSquarenessTreshold = a.getFloat( 153 R.styleable.GestureOverlayView_gestureStrokeSquarenessThreshold, 154 mGestureStrokeSquarenessTreshold); 155 mInterceptEvents = a.getBoolean(R.styleable.GestureOverlayView_eventsInterceptionEnabled, 156 mInterceptEvents); 157 mFadeEnabled = a.getBoolean(R.styleable.GestureOverlayView_fadeEnabled, 158 mFadeEnabled); 159 mOrientation = a.getInt(R.styleable.GestureOverlayView_orientation, mOrientation); 160 161 a.recycle(); 162 163 init(); 164 } 165 166 private void init() { 167 setWillNotDraw(false); 168 169 final Paint gesturePaint = mGesturePaint; 170 gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS); 171 gesturePaint.setColor(mCertainGestureColor); 172 gesturePaint.setStyle(Paint.Style.STROKE); 173 gesturePaint.setStrokeJoin(Paint.Join.ROUND); 174 gesturePaint.setStrokeCap(Paint.Cap.ROUND); 175 gesturePaint.setStrokeWidth(mGestureStrokeWidth); 176 gesturePaint.setDither(DITHER_FLAG); 177 178 mCurrentColor = mCertainGestureColor; 179 setPaintAlpha(255); 180 } 181 182 public ArrayList<GesturePoint> getCurrentStroke() { 183 return mStrokeBuffer; 184 } 185 186 public int getOrientation() { 187 return mOrientation; 188 } 189 190 public void setOrientation(int orientation) { 191 mOrientation = orientation; 192 } 193 194 public void setGestureColor(int color) { 195 mCertainGestureColor = color; 196 } 197 198 public void setUncertainGestureColor(int color) { 199 mUncertainGestureColor = color; 200 } 201 202 public int getUncertainGestureColor() { 203 return mUncertainGestureColor; 204 } 205 206 public int getGestureColor() { 207 return mCertainGestureColor; 208 } 209 210 public float getGestureStrokeWidth() { 211 return mGestureStrokeWidth; 212 } 213 214 public void setGestureStrokeWidth(float gestureStrokeWidth) { 215 mGestureStrokeWidth = gestureStrokeWidth; 216 mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1); 217 mGesturePaint.setStrokeWidth(gestureStrokeWidth); 218 } 219 220 public int getGestureStrokeType() { 221 return mGestureStrokeType; 222 } 223 224 public void setGestureStrokeType(int gestureStrokeType) { 225 mGestureStrokeType = gestureStrokeType; 226 } 227 228 public float getGestureStrokeLengthThreshold() { 229 return mGestureStrokeLengthThreshold; 230 } 231 232 public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) { 233 mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold; 234 } 235 236 public float getGestureStrokeSquarenessTreshold() { 237 return mGestureStrokeSquarenessTreshold; 238 } 239 240 public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) { 241 mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold; 242 } 243 244 public float getGestureStrokeAngleThreshold() { 245 return mGestureStrokeAngleThreshold; 246 } 247 248 public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) { 249 mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold; 250 } 251 252 public boolean isEventsInterceptionEnabled() { 253 return mInterceptEvents; 254 } 255 256 public void setEventsInterceptionEnabled(boolean enabled) { 257 mInterceptEvents = enabled; 258 } 259 260 public boolean isFadeEnabled() { 261 return mFadeEnabled; 262 } 263 264 public void setFadeEnabled(boolean fadeEnabled) { 265 mFadeEnabled = fadeEnabled; 266 } 267 268 public Gesture getGesture() { 269 return mCurrentGesture; 270 } 271 272 public void setGesture(Gesture gesture) { 273 if (mCurrentGesture != null) { 274 clear(false); 275 } 276 277 setCurrentColor(mCertainGestureColor); 278 mCurrentGesture = gesture; 279 280 final Path path = mCurrentGesture.toPath(); 281 final RectF bounds = new RectF(); 282 path.computeBounds(bounds, true); 283 284 mPath.rewind(); 285 mPath.addPath(path, (getWidth() - bounds.width()) / 2.0f, 286 (getHeight() - bounds.height()) / 2.0f); 287 288 invalidate(); 289 } 290 291 public void addOnGestureListener(OnGestureListener listener) { 292 mOnGestureListeners.add(listener); 293 } 294 295 public void removeOnGestureListener(OnGestureListener listener) { 296 mOnGestureListeners.remove(listener); 297 } 298 299 public void removeAllOnGestureListeners() { 300 mOnGestureListeners.clear(); 301 } 302 303 public void addOnGesturePerformedListener(OnGesturePerformedListener listener) { 304 mOnGesturePerformedListeners.add(listener); 305 if (mOnGesturePerformedListeners.size() > 0) { 306 mHandleGestureActions = true; 307 } 308 } 309 310 public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) { 311 mOnGesturePerformedListeners.remove(listener); 312 if (mOnGesturePerformedListeners.size() <= 0) { 313 mHandleGestureActions = false; 314 } 315 } 316 317 public void removeAllOnGesturePerformedListeners() { 318 mOnGesturePerformedListeners.clear(); 319 mHandleGestureActions = false; 320 } 321 322 public boolean isGesturing() { 323 return mIsGesturing; 324 } 325 326 private void setCurrentColor(int color) { 327 mCurrentColor = color; 328 if (mFadingHasStarted) { 329 setPaintAlpha((int) (255 * mFadingAlpha)); 330 } else { 331 setPaintAlpha(255); 332 } 333 invalidate(); 334 } 335 336 @Override 337 public void draw(Canvas canvas) { 338 super.draw(canvas); 339 340 if (mCurrentGesture != null) { 341 canvas.drawPath(mPath, mGesturePaint); 342 } 343 } 344 345 private void setPaintAlpha(int alpha) { 346 alpha += alpha >> 7; 347 final int baseAlpha = mCurrentColor >>> 24; 348 final int useAlpha = baseAlpha * alpha >> 8; 349 mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24)); 350 } 351 352 public void clear(boolean animated) { 353 clear(animated, false); 354 } 355 356 private void clear(boolean animated, boolean fireActionPerformed) { 357 setPaintAlpha(255); 358 removeCallbacks(mFadingOut); 359 mFadingOut.fireActionPerformed = fireActionPerformed; 360 361 if (animated && mCurrentGesture != null) { 362 mFadingAlpha = 1.0f; 363 mIsFadingOut = true; 364 mFadingHasStarted = false; 365 mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset; 366 367 postDelayed(mFadingOut, mFadeOffset); 368 } else { 369 mFadingAlpha = 1.0f; 370 mIsFadingOut = false; 371 mFadingHasStarted = false; 372 373 if (fireActionPerformed) { 374 post(mFadingOut); 375 } else { 376 mCurrentGesture = null; 377 mPath.rewind(); 378 invalidate(); 379 } 380 } 381 } 382 383 public void cancelClearAnimation() { 384 setPaintAlpha(255); 385 mIsFadingOut = false; 386 mFadingHasStarted = false; 387 removeCallbacks(mFadingOut); 388 mPath.rewind(); 389 mCurrentGesture = null; 390 } 391 392 public void cancelGesture() { 393 mIsListeningForGestures = false; 394 395 // add the stroke to the current gesture 396 mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); 397 398 // pass the event to handlers 399 final long now = SystemClock.uptimeMillis(); 400 final MotionEvent event = MotionEvent.obtain(now, now, 401 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); 402 403 final ArrayList<OnGestureListener> listeners = mOnGestureListeners; 404 final int count = listeners.size(); 405 for (int i = 0; i < count; i++) { 406 listeners.get(i).onGestureCancelled(this, event); 407 } 408 409 event.recycle(); 410 411 clear(false); 412 mIsGesturing = false; 413 mStrokeBuffer.clear(); 414 } 415 416 @Override 417 protected void onDetachedFromWindow() { 418 cancelClearAnimation(); 419 } 420 421 @Override 422 public boolean dispatchTouchEvent(MotionEvent event) { 423 if (isEnabled()) { 424 boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null && 425 mCurrentGesture.getStrokesCount() > 0)) && mInterceptEvents; 426 processEvent(event); 427 428 if (cancelDispatch) { 429 event.setAction(MotionEvent.ACTION_CANCEL); 430 } 431 432 super.dispatchTouchEvent(event); 433 return true; 434 } 435 436 return super.dispatchTouchEvent(event); 437 } 438 439 private boolean processEvent(MotionEvent event) { 440 switch (event.getAction()) { 441 case MotionEvent.ACTION_DOWN: 442 touchDown(event); 443 invalidate(); 444 return true; 445 case MotionEvent.ACTION_MOVE: 446 if (mIsListeningForGestures) { 447 Rect rect = touchMove(event); 448 if (rect != null) { 449 invalidate(rect); 450 } 451 return true; 452 } 453 break; 454 case MotionEvent.ACTION_UP: 455 if (mIsListeningForGestures) { 456 touchUp(event, false); 457 invalidate(); 458 return true; 459 } 460 break; 461 case MotionEvent.ACTION_CANCEL: 462 if (mIsListeningForGestures) { 463 touchUp(event, true); 464 invalidate(); 465 return true; 466 } 467 } 468 469 return false; 470 } 471 472 private void touchDown(MotionEvent event) { 473 mIsListeningForGestures = true; 474 475 float x = event.getX(); 476 float y = event.getY(); 477 478 mX = x; 479 mY = y; 480 481 mTotalLength = 0; 482 mIsGesturing = false; 483 484 if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE) { 485 if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); 486 mCurrentGesture = null; 487 mPath.rewind(); 488 } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) { 489 if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); 490 } 491 492 // if there is fading out going on, stop it. 493 if (mFadingHasStarted) { 494 cancelClearAnimation(); 495 } else if (mIsFadingOut) { 496 setPaintAlpha(255); 497 mIsFadingOut = false; 498 mFadingHasStarted = false; 499 removeCallbacks(mFadingOut); 500 } 501 502 if (mCurrentGesture == null) { 503 mCurrentGesture = new Gesture(); 504 } 505 506 mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); 507 mPath.moveTo(x, y); 508 509 final int border = mInvalidateExtraBorder; 510 mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border); 511 512 mCurveEndX = x; 513 mCurveEndY = y; 514 515 // pass the event to handlers 516 final ArrayList<OnGestureListener> listeners = mOnGestureListeners; 517 final int count = listeners.size(); 518 for (int i = 0; i < count; i++) { 519 listeners.get(i).onGestureStarted(this, event); 520 } 521 } 522 523 private Rect touchMove(MotionEvent event) { 524 Rect areaToRefresh = null; 525 526 final float x = event.getX(); 527 final float y = event.getY(); 528 529 final float previousX = mX; 530 final float previousY = mY; 531 532 final float dx = Math.abs(x - previousX); 533 final float dy = Math.abs(y - previousY); 534 535 if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) { 536 areaToRefresh = mInvalidRect; 537 538 // start with the curve end 539 final int border = mInvalidateExtraBorder; 540 areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border, 541 (int) mCurveEndX + border, (int) mCurveEndY + border); 542 543 float cX = mCurveEndX = (x + previousX) / 2; 544 float cY = mCurveEndY = (y + previousY) / 2; 545 546 mPath.quadTo(previousX, previousY, cX, cY); 547 548 // union with the control point of the new curve 549 areaToRefresh.union((int) previousX - border, (int) previousY - border, 550 (int) previousX + border, (int) previousY + border); 551 552 // union with the end point of the new curve 553 areaToRefresh.union((int) cX - border, (int) cY - border, 554 (int) cX + border, (int) cY + border); 555 556 mX = x; 557 mY = y; 558 559 mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); 560 561 if (mHandleGestureActions && !mIsGesturing) { 562 mTotalLength += (float) Math.sqrt(dx * dx + dy * dy); 563 564 if (mTotalLength > mGestureStrokeLengthThreshold) { 565 final OrientedBoundingBox box = 566 GestureUtilities.computeOrientedBoundingBox(mStrokeBuffer); 567 568 float angle = Math.abs(box.orientation); 569 if (angle > 90) { 570 angle = 180 - angle; 571 } 572 573 if (box.squareness > mGestureStrokeSquarenessTreshold || 574 (mOrientation == ORIENTATION_VERTICAL ? 575 angle < mGestureStrokeAngleThreshold : 576 angle > mGestureStrokeAngleThreshold)) { 577 578 mIsGesturing = true; 579 setCurrentColor(mCertainGestureColor); 580 } 581 } 582 } 583 584 // pass the event to handlers 585 final ArrayList<OnGestureListener> listeners = mOnGestureListeners; 586 final int count = listeners.size(); 587 for (int i = 0; i < count; i++) { 588 listeners.get(i).onGesture(this, event); 589 } 590 } 591 592 return areaToRefresh; 593 } 594 595 private void touchUp(MotionEvent event, boolean cancel) { 596 mIsListeningForGestures = false; 597 598 // A gesture wasn't started or was cancelled 599 if (mCurrentGesture != null) { 600 // add the stroke to the current gesture 601 mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); 602 603 if (!cancel) { 604 // pass the event to handlers 605 final ArrayList<OnGestureListener> listeners = mOnGestureListeners; 606 int count = listeners.size(); 607 for (int i = 0; i < count; i++) { 608 listeners.get(i).onGestureEnded(this, event); 609 } 610 611 if (mHandleGestureActions) { 612 clear(mFadeEnabled, mIsGesturing); 613 } 614 } else { 615 cancelGesture(event); 616 617 } 618 } else { 619 cancelGesture(event); 620 } 621 622 mStrokeBuffer.clear(); 623 mIsGesturing = false; 624 } 625 626 private void cancelGesture(MotionEvent event) { 627 // pass the event to handlers 628 final ArrayList<OnGestureListener> listeners = mOnGestureListeners; 629 final int count = listeners.size(); 630 for (int i = 0; i < count; i++) { 631 listeners.get(i).onGestureCancelled(this, event); 632 } 633 634 clear(false); 635 } 636 637 private void fireOnGesturePerformed() { 638 final ArrayList<OnGesturePerformedListener> actionListeners = 639 mOnGesturePerformedListeners; 640 final int count = actionListeners.size(); 641 for (int i = 0; i < count; i++) { 642 actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, 643 mCurrentGesture); 644 } 645 } 646 647 private class FadeOutRunnable implements Runnable { 648 boolean fireActionPerformed; 649 650 public void run() { 651 if (mIsFadingOut) { 652 final long now = AnimationUtils.currentAnimationTimeMillis(); 653 final long duration = now - mFadingStart; 654 655 if (duration > mFadeDuration) { 656 if (fireActionPerformed) { 657 fireOnGesturePerformed(); 658 } 659 660 mIsFadingOut = false; 661 mFadingHasStarted = false; 662 mPath.rewind(); 663 mCurrentGesture = null; 664 setPaintAlpha(255); 665 } else { 666 mFadingHasStarted = true; 667 float interpolatedTime = Math.max(0.0f, 668 Math.min(1.0f, duration / (float) mFadeDuration)); 669 mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime); 670 setPaintAlpha((int) (255 * mFadingAlpha)); 671 postDelayed(this, FADE_ANIMATION_RATE); 672 } 673 } else { 674 fireOnGesturePerformed(); 675 676 mFadingHasStarted = false; 677 mPath.rewind(); 678 mCurrentGesture = null; 679 setPaintAlpha(255); 680 } 681 682 invalidate(); 683 } 684 } 685 686 public static interface OnGestureListener { 687 void onGestureStarted(GestureOverlayView overlay, MotionEvent event); 688 689 void onGesture(GestureOverlayView overlay, MotionEvent event); 690 691 void onGestureEnded(GestureOverlayView overlay, MotionEvent event); 692 693 void onGestureCancelled(GestureOverlayView overlay, MotionEvent event); 694 } 695 696 public static interface OnGesturePerformedListener { 697 void onGesturePerformed(GestureOverlayView overlay, Gesture gesture); 698 } 699} 700