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