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