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