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