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