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 17 package com.android.internal.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Canvas; 23 import android.graphics.Paint; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Matrix; 27 import android.graphics.drawable.Drawable; 28 import android.os.Vibrator; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.VelocityTracker; 34 import android.view.ViewConfiguration; 35 import android.view.animation.DecelerateInterpolator; 36 import static android.view.animation.AnimationUtils.currentAnimationTimeMillis; 37 import com.android.internal.R; 38 39 40 /** 41 * Custom view that presents up to two items that are selectable by rotating a semi-circle from 42 * left to right, or right to left. Used by incoming call screen, and the lock screen when no 43 * security pattern is set. 44 */ 45 public class RotarySelector extends View { 46 public static final int HORIZONTAL = 0; 47 public static final int VERTICAL = 1; 48 49 private static final String LOG_TAG = "RotarySelector"; 50 private static final boolean DBG = false; 51 private static final boolean VISUAL_DEBUG = false; 52 53 // Listener for onDialTrigger() callbacks. 54 private OnDialTriggerListener mOnDialTriggerListener; 55 56 private float mDensity; 57 58 // UI elements 59 private Bitmap mBackground; 60 private Bitmap mDimple; 61 private Bitmap mDimpleDim; 62 63 private Bitmap mLeftHandleIcon; 64 private Bitmap mRightHandleIcon; 65 66 private Bitmap mArrowShortLeftAndRight; 67 private Bitmap mArrowLongLeft; // Long arrow starting on the left, pointing clockwise 68 private Bitmap mArrowLongRight; // Long arrow starting on the right, pointing CCW 69 70 // positions of the left and right handle 71 private int mLeftHandleX; 72 private int mRightHandleX; 73 74 // current offset of rotary widget along the x axis 75 private int mRotaryOffsetX = 0; 76 77 // state of the animation used to bring the handle back to its start position when 78 // the user lets go before triggering an action 79 private boolean mAnimating = false; 80 private long mAnimationStartTime; 81 private long mAnimationDuration; 82 private int mAnimatingDeltaXStart; // the animation will interpolate from this delta to zero 83 private int mAnimatingDeltaXEnd; 84 85 private DecelerateInterpolator mInterpolator; 86 87 private Paint mPaint = new Paint(); 88 89 // used to rotate the background and arrow assets depending on orientation 90 final Matrix mBgMatrix = new Matrix(); 91 final Matrix mArrowMatrix = new Matrix(); 92 93 /** 94 * If the user is currently dragging something. 95 */ 96 private int mGrabbedState = NOTHING_GRABBED; 97 public static final int NOTHING_GRABBED = 0; 98 public static final int LEFT_HANDLE_GRABBED = 1; 99 public static final int RIGHT_HANDLE_GRABBED = 2; 100 101 /** 102 * Whether the user has triggered something (e.g dragging the left handle all the way over to 103 * the right). 104 */ 105 private boolean mTriggered = false; 106 107 // Vibration (haptic feedback) 108 private Vibrator mVibrator; 109 private static final long VIBRATE_SHORT = 20; // msec 110 private static final long VIBRATE_LONG = 20; // msec 111 112 /** 113 * The drawable for the arrows need to be scrunched this many dips towards the rotary bg below 114 * it. 115 */ 116 private static final int ARROW_SCRUNCH_DIP = 6; 117 118 /** 119 * How far inset the left and right circles should be 120 */ 121 private static final int EDGE_PADDING_DIP = 9; 122 123 /** 124 * How far from the edge of the screen the user must drag to trigger the event. 125 */ 126 private static final int EDGE_TRIGGER_DIP = 100; 127 128 /** 129 * Dimensions of arc in background drawable. 130 */ 131 static final int OUTER_ROTARY_RADIUS_DIP = 390; 132 static final int ROTARY_STROKE_WIDTH_DIP = 83; 133 static final int SNAP_BACK_ANIMATION_DURATION_MILLIS = 300; 134 static final int SPIN_ANIMATION_DURATION_MILLIS = 800; 135 136 private int mEdgeTriggerThresh; 137 private int mDimpleWidth; 138 private int mBackgroundWidth; 139 private int mBackgroundHeight; 140 private final int mOuterRadius; 141 private final int mInnerRadius; 142 private int mDimpleSpacing; 143 144 private VelocityTracker mVelocityTracker; 145 private int mMinimumVelocity; 146 private int mMaximumVelocity; 147 148 /** 149 * The number of dimples we are flinging when we do the "spin" animation. Used to know when to 150 * wrap the icons back around so they "rotate back" onto the screen. 151 * @see #updateAnimation() 152 */ 153 private int mDimplesOfFling = 0; 154 155 /** 156 * Either {@link #HORIZONTAL} or {@link #VERTICAL}. 157 */ 158 private int mOrientation; 159 160 161 public RotarySelector(Context context) { 162 this(context, null); 163 } 164 165 /** 166 * Constructor used when this widget is created from a layout file. 167 */ 168 public RotarySelector(Context context, AttributeSet attrs) { 169 super(context, attrs); 170 171 TypedArray a = 172 context.obtainStyledAttributes(attrs, R.styleable.RotarySelector); 173 mOrientation = a.getInt(R.styleable.RotarySelector_orientation, HORIZONTAL); 174 a.recycle(); 175 176 Resources r = getResources(); 177 mDensity = r.getDisplayMetrics().density; 178 if (DBG) log("- Density: " + mDensity); 179 180 // Assets (all are BitmapDrawables). 181 mBackground = getBitmapFor(R.drawable.jog_dial_bg); 182 mDimple = getBitmapFor(R.drawable.jog_dial_dimple); 183 mDimpleDim = getBitmapFor(R.drawable.jog_dial_dimple_dim); 184 185 mArrowLongLeft = getBitmapFor(R.drawable.jog_dial_arrow_long_left_green); 186 mArrowLongRight = getBitmapFor(R.drawable.jog_dial_arrow_long_right_red); 187 mArrowShortLeftAndRight = getBitmapFor(R.drawable.jog_dial_arrow_short_left_and_right); 188 189 mInterpolator = new DecelerateInterpolator(1f); 190 191 mEdgeTriggerThresh = (int) (mDensity * EDGE_TRIGGER_DIP); 192 193 mDimpleWidth = mDimple.getWidth(); 194 195 mBackgroundWidth = mBackground.getWidth(); 196 mBackgroundHeight = mBackground.getHeight(); 197 mOuterRadius = (int) (mDensity * OUTER_ROTARY_RADIUS_DIP); 198 mInnerRadius = (int) ((OUTER_ROTARY_RADIUS_DIP - ROTARY_STROKE_WIDTH_DIP) * mDensity); 199 200 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 201 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity() * 2; 202 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 203 } 204 205 private Bitmap getBitmapFor(int resId) { 206 return BitmapFactory.decodeResource(getContext().getResources(), resId); 207 } 208 209 @Override 210 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 211 super.onSizeChanged(w, h, oldw, oldh); 212 213 final int edgePadding = (int) (EDGE_PADDING_DIP * mDensity); 214 mLeftHandleX = edgePadding + mDimpleWidth / 2; 215 final int length = isHoriz() ? w : h; 216 mRightHandleX = length - edgePadding - mDimpleWidth / 2; 217 mDimpleSpacing = (length / 2) - mLeftHandleX; 218 219 // bg matrix only needs to be calculated once 220 mBgMatrix.setTranslate(0, 0); 221 if (!isHoriz()) { 222 // set up matrix for translating drawing of background and arrow assets 223 final int left = w - mBackgroundHeight; 224 mBgMatrix.preRotate(-90, 0, 0); 225 mBgMatrix.postTranslate(left, h); 226 227 } else { 228 mBgMatrix.postTranslate(0, h - mBackgroundHeight); 229 } 230 } 231 232 private boolean isHoriz() { 233 return mOrientation == HORIZONTAL; 234 } 235 236 /** 237 * Sets the left handle icon to a given resource. 238 * 239 * The resource should refer to a Drawable object, or use 0 to remove 240 * the icon. 241 * 242 * @param resId the resource ID. 243 */ 244 public void setLeftHandleResource(int resId) { 245 if (resId != 0) { 246 mLeftHandleIcon = getBitmapFor(resId); 247 } 248 invalidate(); 249 } 250 251 /** 252 * Sets the right handle icon to a given resource. 253 * 254 * The resource should refer to a Drawable object, or use 0 to remove 255 * the icon. 256 * 257 * @param resId the resource ID. 258 */ 259 public void setRightHandleResource(int resId) { 260 if (resId != 0) { 261 mRightHandleIcon = getBitmapFor(resId); 262 } 263 invalidate(); 264 } 265 266 267 @Override 268 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 269 final int length = isHoriz() ? 270 MeasureSpec.getSize(widthMeasureSpec) : 271 MeasureSpec.getSize(heightMeasureSpec); 272 final int arrowScrunch = (int) (ARROW_SCRUNCH_DIP * mDensity); 273 final int arrowH = mArrowShortLeftAndRight.getHeight(); 274 275 // by making the height less than arrow + bg, arrow and bg will be scrunched together, 276 // overlaying somewhat (though on transparent portions of the drawable). 277 // this works because the arrows are drawn from the top, and the rotary bg is drawn 278 // from the bottom. 279 final int height = mBackgroundHeight + arrowH - arrowScrunch; 280 281 if (isHoriz()) { 282 setMeasuredDimension(length, height); 283 } else { 284 setMeasuredDimension(height, length); 285 } 286 } 287 288 @Override 289 protected void onDraw(Canvas canvas) { 290 super.onDraw(canvas); 291 292 final int width = getWidth(); 293 294 if (VISUAL_DEBUG) { 295 // draw bounding box around widget 296 mPaint.setColor(0xffff0000); 297 mPaint.setStyle(Paint.Style.STROKE); 298 canvas.drawRect(0, 0, width, getHeight(), mPaint); 299 } 300 301 final int height = getHeight(); 302 303 // update animating state before we draw anything 304 if (mAnimating) { 305 updateAnimation(); 306 } 307 308 // Background: 309 canvas.drawBitmap(mBackground, mBgMatrix, mPaint); 310 311 // Draw the correct arrow(s) depending on the current state: 312 mArrowMatrix.reset(); 313 switch (mGrabbedState) { 314 case NOTHING_GRABBED: 315 //mArrowShortLeftAndRight; 316 break; 317 case LEFT_HANDLE_GRABBED: 318 mArrowMatrix.setTranslate(0, 0); 319 if (!isHoriz()) { 320 mArrowMatrix.preRotate(-90, 0, 0); 321 mArrowMatrix.postTranslate(0, height); 322 } 323 canvas.drawBitmap(mArrowLongLeft, mArrowMatrix, mPaint); 324 break; 325 case RIGHT_HANDLE_GRABBED: 326 mArrowMatrix.setTranslate(0, 0); 327 if (!isHoriz()) { 328 mArrowMatrix.preRotate(-90, 0, 0); 329 // since bg width is > height of screen in landscape mode... 330 mArrowMatrix.postTranslate(0, height + (mBackgroundWidth - height)); 331 } 332 canvas.drawBitmap(mArrowLongRight, mArrowMatrix, mPaint); 333 break; 334 default: 335 throw new IllegalStateException("invalid mGrabbedState: " + mGrabbedState); 336 } 337 338 final int bgHeight = mBackgroundHeight; 339 final int bgTop = isHoriz() ? 340 height - bgHeight: 341 width - bgHeight; 342 343 if (VISUAL_DEBUG) { 344 // draw circle bounding arc drawable: good sanity check we're doing the math correctly 345 float or = OUTER_ROTARY_RADIUS_DIP * mDensity; 346 final int vOffset = mBackgroundWidth - height; 347 final int midX = isHoriz() ? width / 2 : mBackgroundWidth / 2 - vOffset; 348 if (isHoriz()) { 349 canvas.drawCircle(midX, or + bgTop, or, mPaint); 350 } else { 351 canvas.drawCircle(or + bgTop, midX, or, mPaint); 352 } 353 } 354 355 // left dimple / icon 356 { 357 final int xOffset = mLeftHandleX + mRotaryOffsetX; 358 final int drawableY = getYOnArc( 359 mBackgroundWidth, 360 mInnerRadius, 361 mOuterRadius, 362 xOffset); 363 final int x = isHoriz() ? xOffset : drawableY + bgTop; 364 final int y = isHoriz() ? drawableY + bgTop : height - xOffset; 365 if (mGrabbedState != RIGHT_HANDLE_GRABBED) { 366 drawCentered(mDimple, canvas, x, y); 367 drawCentered(mLeftHandleIcon, canvas, x, y); 368 } else { 369 drawCentered(mDimpleDim, canvas, x, y); 370 } 371 } 372 373 // center dimple 374 { 375 final int xOffset = isHoriz() ? 376 width / 2 + mRotaryOffsetX: 377 height / 2 + mRotaryOffsetX; 378 final int drawableY = getYOnArc( 379 mBackgroundWidth, 380 mInnerRadius, 381 mOuterRadius, 382 xOffset); 383 384 if (isHoriz()) { 385 drawCentered(mDimpleDim, canvas, xOffset, drawableY + bgTop); 386 } else { 387 // vertical 388 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - xOffset); 389 } 390 } 391 392 // right dimple / icon 393 { 394 final int xOffset = mRightHandleX + mRotaryOffsetX; 395 final int drawableY = getYOnArc( 396 mBackgroundWidth, 397 mInnerRadius, 398 mOuterRadius, 399 xOffset); 400 401 final int x = isHoriz() ? xOffset : drawableY + bgTop; 402 final int y = isHoriz() ? drawableY + bgTop : height - xOffset; 403 if (mGrabbedState != LEFT_HANDLE_GRABBED) { 404 drawCentered(mDimple, canvas, x, y); 405 drawCentered(mRightHandleIcon, canvas, x, y); 406 } else { 407 drawCentered(mDimpleDim, canvas, x, y); 408 } 409 } 410 411 // draw extra left hand dimples 412 int dimpleLeft = mRotaryOffsetX + mLeftHandleX - mDimpleSpacing; 413 final int halfdimple = mDimpleWidth / 2; 414 while (dimpleLeft > -halfdimple) { 415 final int drawableY = getYOnArc( 416 mBackgroundWidth, 417 mInnerRadius, 418 mOuterRadius, 419 dimpleLeft); 420 421 if (isHoriz()) { 422 drawCentered(mDimpleDim, canvas, dimpleLeft, drawableY + bgTop); 423 } else { 424 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleLeft); 425 } 426 dimpleLeft -= mDimpleSpacing; 427 } 428 429 // draw extra right hand dimples 430 int dimpleRight = mRotaryOffsetX + mRightHandleX + mDimpleSpacing; 431 final int rightThresh = mRight + halfdimple; 432 while (dimpleRight < rightThresh) { 433 final int drawableY = getYOnArc( 434 mBackgroundWidth, 435 mInnerRadius, 436 mOuterRadius, 437 dimpleRight); 438 439 if (isHoriz()) { 440 drawCentered(mDimpleDim, canvas, dimpleRight, drawableY + bgTop); 441 } else { 442 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleRight); 443 } 444 dimpleRight += mDimpleSpacing; 445 } 446 } 447 448 /** 449 * Assuming bitmap is a bounding box around a piece of an arc drawn by two concentric circles 450 * (as the background drawable for the rotary widget is), and given an x coordinate along the 451 * drawable, return the y coordinate of a point on the arc that is between the two concentric 452 * circles. The resulting y combined with the incoming x is a point along the circle in 453 * between the two concentric circles. 454 * 455 * @param backgroundWidth The width of the asset (the bottom of the box surrounding the arc). 456 * @param innerRadius The radius of the circle that intersects the drawable at the bottom two 457 * corders of the drawable (top two corners in terms of drawing coordinates). 458 * @param outerRadius The radius of the circle who's top most point is the top center of the 459 * drawable (bottom center in terms of drawing coordinates). 460 * @param x The distance along the x axis of the desired point. @return The y coordinate, in drawing coordinates, that will place (x, y) along the circle 461 * in between the two concentric circles. 462 */ 463 private int getYOnArc(int backgroundWidth, int innerRadius, int outerRadius, int x) { 464 465 // the hypotenuse 466 final int halfWidth = (outerRadius - innerRadius) / 2; 467 final int middleRadius = innerRadius + halfWidth; 468 469 // the bottom leg of the triangle 470 final int triangleBottom = (backgroundWidth / 2) - x; 471 472 // "Our offense is like the pythagorean theorem: There is no answer!" - Shaquille O'Neal 473 final int triangleY = 474 (int) Math.sqrt(middleRadius * middleRadius - triangleBottom * triangleBottom); 475 476 // convert to drawing coordinates: 477 // middleRadius - triangleY = 478 // the vertical distance from the outer edge of the circle to the desired point 479 // from there we add the distance from the top of the drawable to the middle circle 480 return middleRadius - triangleY + halfWidth; 481 } 482 483 /** 484 * Handle touch screen events. 485 * 486 * @param event The motion event. 487 * @return True if the event was handled, false otherwise. 488 */ 489 @Override 490 public boolean onTouchEvent(MotionEvent event) { 491 if (mAnimating) { 492 return true; 493 } 494 if (mVelocityTracker == null) { 495 mVelocityTracker = VelocityTracker.obtain(); 496 } 497 mVelocityTracker.addMovement(event); 498 499 final int height = getHeight(); 500 501 final int eventX = isHoriz() ? 502 (int) event.getX(): 503 height - ((int) event.getY()); 504 final int hitWindow = mDimpleWidth; 505 506 final int action = event.getAction(); 507 switch (action) { 508 case MotionEvent.ACTION_DOWN: 509 if (DBG) log("touch-down"); 510 mTriggered = false; 511 if (mGrabbedState != NOTHING_GRABBED) { 512 reset(); 513 invalidate(); 514 } 515 if (eventX < mLeftHandleX + hitWindow) { 516 mRotaryOffsetX = eventX - mLeftHandleX; 517 setGrabbedState(LEFT_HANDLE_GRABBED); 518 invalidate(); 519 vibrate(VIBRATE_SHORT); 520 } else if (eventX > mRightHandleX - hitWindow) { 521 mRotaryOffsetX = eventX - mRightHandleX; 522 setGrabbedState(RIGHT_HANDLE_GRABBED); 523 invalidate(); 524 vibrate(VIBRATE_SHORT); 525 } 526 break; 527 528 case MotionEvent.ACTION_MOVE: 529 if (DBG) log("touch-move"); 530 if (mGrabbedState == LEFT_HANDLE_GRABBED) { 531 mRotaryOffsetX = eventX - mLeftHandleX; 532 invalidate(); 533 final int rightThresh = isHoriz() ? getRight() : height; 534 if (eventX >= rightThresh - mEdgeTriggerThresh && !mTriggered) { 535 mTriggered = true; 536 dispatchTriggerEvent(OnDialTriggerListener.LEFT_HANDLE); 537 final VelocityTracker velocityTracker = mVelocityTracker; 538 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 539 final int rawVelocity = isHoriz() ? 540 (int) velocityTracker.getXVelocity(): 541 -(int) velocityTracker.getYVelocity(); 542 final int velocity = Math.max(mMinimumVelocity, rawVelocity); 543 mDimplesOfFling = Math.max( 544 8, 545 Math.abs(velocity / mDimpleSpacing)); 546 startAnimationWithVelocity( 547 eventX - mLeftHandleX, 548 mDimplesOfFling * mDimpleSpacing, 549 velocity); 550 } 551 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED) { 552 mRotaryOffsetX = eventX - mRightHandleX; 553 invalidate(); 554 if (eventX <= mEdgeTriggerThresh && !mTriggered) { 555 mTriggered = true; 556 dispatchTriggerEvent(OnDialTriggerListener.RIGHT_HANDLE); 557 final VelocityTracker velocityTracker = mVelocityTracker; 558 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 559 final int rawVelocity = isHoriz() ? 560 (int) velocityTracker.getXVelocity(): 561 - (int) velocityTracker.getYVelocity(); 562 final int velocity = Math.min(-mMinimumVelocity, rawVelocity); 563 mDimplesOfFling = Math.max( 564 8, 565 Math.abs(velocity / mDimpleSpacing)); 566 startAnimationWithVelocity( 567 eventX - mRightHandleX, 568 -(mDimplesOfFling * mDimpleSpacing), 569 velocity); 570 } 571 } 572 break; 573 case MotionEvent.ACTION_UP: 574 if (DBG) log("touch-up"); 575 // handle animating back to start if they didn't trigger 576 if (mGrabbedState == LEFT_HANDLE_GRABBED 577 && Math.abs(eventX - mLeftHandleX) > 5) { 578 // set up "snap back" animation 579 startAnimation(eventX - mLeftHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS); 580 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED 581 && Math.abs(eventX - mRightHandleX) > 5) { 582 // set up "snap back" animation 583 startAnimation(eventX - mRightHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS); 584 } 585 mRotaryOffsetX = 0; 586 setGrabbedState(NOTHING_GRABBED); 587 invalidate(); 588 if (mVelocityTracker != null) { 589 mVelocityTracker.recycle(); // wishin' we had generational GC 590 mVelocityTracker = null; 591 } 592 break; 593 case MotionEvent.ACTION_CANCEL: 594 if (DBG) log("touch-cancel"); 595 reset(); 596 invalidate(); 597 if (mVelocityTracker != null) { 598 mVelocityTracker.recycle(); 599 mVelocityTracker = null; 600 } 601 break; 602 } 603 return true; 604 } 605 606 private void startAnimation(int startX, int endX, int duration) { 607 mAnimating = true; 608 mAnimationStartTime = currentAnimationTimeMillis(); 609 mAnimationDuration = duration; 610 mAnimatingDeltaXStart = startX; 611 mAnimatingDeltaXEnd = endX; 612 setGrabbedState(NOTHING_GRABBED); 613 mDimplesOfFling = 0; 614 invalidate(); 615 } 616 617 private void startAnimationWithVelocity(int startX, int endX, int pixelsPerSecond) { 618 mAnimating = true; 619 mAnimationStartTime = currentAnimationTimeMillis(); 620 mAnimationDuration = 1000 * (endX - startX) / pixelsPerSecond; 621 mAnimatingDeltaXStart = startX; 622 mAnimatingDeltaXEnd = endX; 623 setGrabbedState(NOTHING_GRABBED); 624 invalidate(); 625 } 626 627 private void updateAnimation() { 628 final long millisSoFar = currentAnimationTimeMillis() - mAnimationStartTime; 629 final long millisLeft = mAnimationDuration - millisSoFar; 630 final int totalDeltaX = mAnimatingDeltaXStart - mAnimatingDeltaXEnd; 631 final boolean goingRight = totalDeltaX < 0; 632 if (DBG) log("millisleft for animating: " + millisLeft); 633 if (millisLeft <= 0) { 634 reset(); 635 return; 636 } 637 // from 0 to 1 as animation progresses 638 float interpolation = 639 mInterpolator.getInterpolation((float) millisSoFar / mAnimationDuration); 640 final int dx = (int) (totalDeltaX * (1 - interpolation)); 641 mRotaryOffsetX = mAnimatingDeltaXEnd + dx; 642 643 // once we have gone far enough to animate the current buttons off screen, we start 644 // wrapping the offset back to the other side so that when the animation is finished, 645 // the buttons will come back into their original places. 646 if (mDimplesOfFling > 0) { 647 if (!goingRight && mRotaryOffsetX < -3 * mDimpleSpacing) { 648 // wrap around on fling left 649 mRotaryOffsetX += mDimplesOfFling * mDimpleSpacing; 650 } else if (goingRight && mRotaryOffsetX > 3 * mDimpleSpacing) { 651 // wrap around on fling right 652 mRotaryOffsetX -= mDimplesOfFling * mDimpleSpacing; 653 } 654 } 655 invalidate(); 656 } 657 658 private void reset() { 659 mAnimating = false; 660 mRotaryOffsetX = 0; 661 mDimplesOfFling = 0; 662 setGrabbedState(NOTHING_GRABBED); 663 mTriggered = false; 664 } 665 666 /** 667 * Triggers haptic feedback. 668 */ 669 private synchronized void vibrate(long duration) { 670 if (mVibrator == null) { 671 mVibrator = (android.os.Vibrator) 672 getContext().getSystemService(Context.VIBRATOR_SERVICE); 673 } 674 mVibrator.vibrate(duration); 675 } 676 677 /** 678 * Draw the bitmap so that it's centered 679 * on the point (x,y), then draws it using specified canvas. 680 * TODO: is there already a utility method somewhere for this? 681 */ 682 private void drawCentered(Bitmap d, Canvas c, int x, int y) { 683 int w = d.getWidth(); 684 int h = d.getHeight(); 685 686 c.drawBitmap(d, x - (w / 2), y - (h / 2), mPaint); 687 } 688 689 690 /** 691 * Registers a callback to be invoked when the dial 692 * is "triggered" by rotating it one way or the other. 693 * 694 * @param l the OnDialTriggerListener to attach to this view 695 */ 696 public void setOnDialTriggerListener(OnDialTriggerListener l) { 697 mOnDialTriggerListener = l; 698 } 699 700 /** 701 * Dispatches a trigger event to our listener. 702 */ 703 private void dispatchTriggerEvent(int whichHandle) { 704 vibrate(VIBRATE_LONG); 705 if (mOnDialTriggerListener != null) { 706 mOnDialTriggerListener.onDialTrigger(this, whichHandle); 707 } 708 } 709 710 /** 711 * Sets the current grabbed state, and dispatches a grabbed state change 712 * event to our listener. 713 */ 714 private void setGrabbedState(int newState) { 715 if (newState != mGrabbedState) { 716 mGrabbedState = newState; 717 if (mOnDialTriggerListener != null) { 718 mOnDialTriggerListener.onGrabbedStateChange(this, mGrabbedState); 719 } 720 } 721 } 722 723 /** 724 * Interface definition for a callback to be invoked when the dial 725 * is "triggered" by rotating it one way or the other. 726 */ 727 public interface OnDialTriggerListener { 728 /** 729 * The dial was triggered because the user grabbed the left handle, 730 * and rotated the dial clockwise. 731 */ 732 public static final int LEFT_HANDLE = 1; 733 734 /** 735 * The dial was triggered because the user grabbed the right handle, 736 * and rotated the dial counterclockwise. 737 */ 738 public static final int RIGHT_HANDLE = 2; 739 740 /** 741 * Called when the dial is triggered. 742 * 743 * @param v The view that was triggered 744 * @param whichHandle Which "dial handle" the user grabbed, 745 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}. 746 */ 747 void onDialTrigger(View v, int whichHandle); 748 749 /** 750 * Called when the "grabbed state" changes (i.e. when 751 * the user either grabs or releases one of the handles.) 752 * 753 * @param v the view that was triggered 754 * @param grabbedState the new state: either {@link #NOTHING_GRABBED}, 755 * {@link #LEFT_HANDLE_GRABBED}, or {@link #RIGHT_HANDLE_GRABBED}. 756 */ 757 void onGrabbedStateChange(View v, int grabbedState); 758 } 759 760 761 // Debugging / testing code 762 763 private void log(String msg) { 764 Log.d(LOG_TAG, msg); 765 } 766 } 767