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