Home | History | Annotate | Download | only in widget
      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 
    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      */
    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 
    214     private Bitmap getBitmapFor(int resId) {
    215         return BitmapFactory.decodeResource(getContext().getResources(), resId);
    216     }
    217 
    218     @Override
    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 
    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      */
    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      */
    268     public void setRightHandleResource(int resId) {
    269         if (resId != 0) {
    270             mRightHandleIcon = getBitmapFor(resId);
    271         }
    272         invalidate();
    273     }
    274 
    275 
    276     @Override
    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
    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      */
    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
    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 
    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 
    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 
    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