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