| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.widget; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Matrix; |
| import android.graphics.drawable.Drawable; |
| import android.os.Vibrator; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.VelocityTracker; |
| import android.view.ViewConfiguration; |
| import android.view.animation.DecelerateInterpolator; |
| import static android.view.animation.AnimationUtils.currentAnimationTimeMillis; |
| import com.android.internal.R; |
| |
| |
| /** |
| * Custom view that presents up to two items that are selectable by rotating a semi-circle from |
| * left to right, or right to left. Used by incoming call screen, and the lock screen when no |
| * security pattern is set. |
| */ |
| public class RotarySelector extends View { |
| public static final int HORIZONTAL = 0; |
| public static final int VERTICAL = 1; |
| |
| private static final String LOG_TAG = "RotarySelector"; |
| private static final boolean DBG = false; |
| private static final boolean VISUAL_DEBUG = false; |
| |
| // Listener for onDialTrigger() callbacks. |
| private OnDialTriggerListener mOnDialTriggerListener; |
| |
| private float mDensity; |
| |
| // UI elements |
| private Bitmap mBackground; |
| private Bitmap mDimple; |
| private Bitmap mDimpleDim; |
| |
| private Bitmap mLeftHandleIcon; |
| private Bitmap mRightHandleIcon; |
| |
| private Bitmap mArrowShortLeftAndRight; |
| private Bitmap mArrowLongLeft; // Long arrow starting on the left, pointing clockwise |
| private Bitmap mArrowLongRight; // Long arrow starting on the right, pointing CCW |
| |
| // positions of the left and right handle |
| private int mLeftHandleX; |
| private int mRightHandleX; |
| |
| // current offset of rotary widget along the x axis |
| private int mRotaryOffsetX = 0; |
| |
| // state of the animation used to bring the handle back to its start position when |
| // the user lets go before triggering an action |
| private boolean mAnimating = false; |
| private long mAnimationStartTime; |
| private long mAnimationDuration; |
| private int mAnimatingDeltaXStart; // the animation will interpolate from this delta to zero |
| private int mAnimatingDeltaXEnd; |
| |
| private DecelerateInterpolator mInterpolator; |
| |
| private Paint mPaint = new Paint(); |
| |
| // used to rotate the background and arrow assets depending on orientation |
| final Matrix mBgMatrix = new Matrix(); |
| final Matrix mArrowMatrix = new Matrix(); |
| |
| /** |
| * If the user is currently dragging something. |
| */ |
| private int mGrabbedState = NOTHING_GRABBED; |
| public static final int NOTHING_GRABBED = 0; |
| public static final int LEFT_HANDLE_GRABBED = 1; |
| public static final int RIGHT_HANDLE_GRABBED = 2; |
| |
| /** |
| * Whether the user has triggered something (e.g dragging the left handle all the way over to |
| * the right). |
| */ |
| private boolean mTriggered = false; |
| |
| // Vibration (haptic feedback) |
| private Vibrator mVibrator; |
| private static final long VIBRATE_SHORT = 20; // msec |
| private static final long VIBRATE_LONG = 20; // msec |
| |
| /** |
| * The drawable for the arrows need to be scrunched this many dips towards the rotary bg below |
| * it. |
| */ |
| private static final int ARROW_SCRUNCH_DIP = 6; |
| |
| /** |
| * How far inset the left and right circles should be |
| */ |
| private static final int EDGE_PADDING_DIP = 9; |
| |
| /** |
| * How far from the edge of the screen the user must drag to trigger the event. |
| */ |
| private static final int EDGE_TRIGGER_DIP = 100; |
| |
| /** |
| * Dimensions of arc in background drawable. |
| */ |
| static final int OUTER_ROTARY_RADIUS_DIP = 390; |
| static final int ROTARY_STROKE_WIDTH_DIP = 83; |
| static final int SNAP_BACK_ANIMATION_DURATION_MILLIS = 300; |
| static final int SPIN_ANIMATION_DURATION_MILLIS = 800; |
| |
| private int mEdgeTriggerThresh; |
| private int mDimpleWidth; |
| private int mBackgroundWidth; |
| private int mBackgroundHeight; |
| private final int mOuterRadius; |
| private final int mInnerRadius; |
| private int mDimpleSpacing; |
| |
| private VelocityTracker mVelocityTracker; |
| private int mMinimumVelocity; |
| private int mMaximumVelocity; |
| |
| /** |
| * The number of dimples we are flinging when we do the "spin" animation. Used to know when to |
| * wrap the icons back around so they "rotate back" onto the screen. |
| * @see #updateAnimation() |
| */ |
| private int mDimplesOfFling = 0; |
| |
| /** |
| * Either {@link #HORIZONTAL} or {@link #VERTICAL}. |
| */ |
| private int mOrientation; |
| |
| |
| public RotarySelector(Context context) { |
| this(context, null); |
| } |
| |
| /** |
| * Constructor used when this widget is created from a layout file. |
| */ |
| public RotarySelector(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| |
| TypedArray a = |
| context.obtainStyledAttributes(attrs, R.styleable.RotarySelector); |
| mOrientation = a.getInt(R.styleable.RotarySelector_orientation, HORIZONTAL); |
| a.recycle(); |
| |
| Resources r = getResources(); |
| mDensity = r.getDisplayMetrics().density; |
| if (DBG) log("- Density: " + mDensity); |
| |
| // Assets (all are BitmapDrawables). |
| mBackground = getBitmapFor(R.drawable.jog_dial_bg); |
| mDimple = getBitmapFor(R.drawable.jog_dial_dimple); |
| mDimpleDim = getBitmapFor(R.drawable.jog_dial_dimple_dim); |
| |
| mArrowLongLeft = getBitmapFor(R.drawable.jog_dial_arrow_long_left_green); |
| mArrowLongRight = getBitmapFor(R.drawable.jog_dial_arrow_long_right_red); |
| mArrowShortLeftAndRight = getBitmapFor(R.drawable.jog_dial_arrow_short_left_and_right); |
| |
| mInterpolator = new DecelerateInterpolator(1f); |
| |
| mEdgeTriggerThresh = (int) (mDensity * EDGE_TRIGGER_DIP); |
| |
| mDimpleWidth = mDimple.getWidth(); |
| |
| mBackgroundWidth = mBackground.getWidth(); |
| mBackgroundHeight = mBackground.getHeight(); |
| mOuterRadius = (int) (mDensity * OUTER_ROTARY_RADIUS_DIP); |
| mInnerRadius = (int) ((OUTER_ROTARY_RADIUS_DIP - ROTARY_STROKE_WIDTH_DIP) * mDensity); |
| |
| final ViewConfiguration configuration = ViewConfiguration.get(mContext); |
| mMinimumVelocity = configuration.getScaledMinimumFlingVelocity() * 2; |
| mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); |
| } |
| |
| private Bitmap getBitmapFor(int resId) { |
| return BitmapFactory.decodeResource(getContext().getResources(), resId); |
| } |
| |
| @Override |
| protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| super.onSizeChanged(w, h, oldw, oldh); |
| |
| final int edgePadding = (int) (EDGE_PADDING_DIP * mDensity); |
| mLeftHandleX = edgePadding + mDimpleWidth / 2; |
| final int length = isHoriz() ? w : h; |
| mRightHandleX = length - edgePadding - mDimpleWidth / 2; |
| mDimpleSpacing = (length / 2) - mLeftHandleX; |
| |
| // bg matrix only needs to be calculated once |
| mBgMatrix.setTranslate(0, 0); |
| if (!isHoriz()) { |
| // set up matrix for translating drawing of background and arrow assets |
| final int left = w - mBackgroundHeight; |
| mBgMatrix.preRotate(-90, 0, 0); |
| mBgMatrix.postTranslate(left, h); |
| |
| } else { |
| mBgMatrix.postTranslate(0, h - mBackgroundHeight); |
| } |
| } |
| |
| private boolean isHoriz() { |
| return mOrientation == HORIZONTAL; |
| } |
| |
| /** |
| * Sets the left handle icon to a given resource. |
| * |
| * The resource should refer to a Drawable object, or use 0 to remove |
| * the icon. |
| * |
| * @param resId the resource ID. |
| */ |
| public void setLeftHandleResource(int resId) { |
| if (resId != 0) { |
| mLeftHandleIcon = getBitmapFor(resId); |
| } |
| invalidate(); |
| } |
| |
| /** |
| * Sets the right handle icon to a given resource. |
| * |
| * The resource should refer to a Drawable object, or use 0 to remove |
| * the icon. |
| * |
| * @param resId the resource ID. |
| */ |
| public void setRightHandleResource(int resId) { |
| if (resId != 0) { |
| mRightHandleIcon = getBitmapFor(resId); |
| } |
| invalidate(); |
| } |
| |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int length = isHoriz() ? |
| MeasureSpec.getSize(widthMeasureSpec) : |
| MeasureSpec.getSize(heightMeasureSpec); |
| final int arrowScrunch = (int) (ARROW_SCRUNCH_DIP * mDensity); |
| final int arrowH = mArrowShortLeftAndRight.getHeight(); |
| |
| // by making the height less than arrow + bg, arrow and bg will be scrunched together, |
| // overlaying somewhat (though on transparent portions of the drawable). |
| // this works because the arrows are drawn from the top, and the rotary bg is drawn |
| // from the bottom. |
| final int height = mBackgroundHeight + arrowH - arrowScrunch; |
| |
| if (isHoriz()) { |
| setMeasuredDimension(length, height); |
| } else { |
| setMeasuredDimension(height, length); |
| } |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| final int width = getWidth(); |
| |
| if (VISUAL_DEBUG) { |
| // draw bounding box around widget |
| mPaint.setColor(0xffff0000); |
| mPaint.setStyle(Paint.Style.STROKE); |
| canvas.drawRect(0, 0, width, getHeight(), mPaint); |
| } |
| |
| final int height = getHeight(); |
| |
| // update animating state before we draw anything |
| if (mAnimating) { |
| updateAnimation(); |
| } |
| |
| // Background: |
| canvas.drawBitmap(mBackground, mBgMatrix, mPaint); |
| |
| // Draw the correct arrow(s) depending on the current state: |
| mArrowMatrix.reset(); |
| switch (mGrabbedState) { |
| case NOTHING_GRABBED: |
| //mArrowShortLeftAndRight; |
| break; |
| case LEFT_HANDLE_GRABBED: |
| mArrowMatrix.setTranslate(0, 0); |
| if (!isHoriz()) { |
| mArrowMatrix.preRotate(-90, 0, 0); |
| mArrowMatrix.postTranslate(0, height); |
| } |
| canvas.drawBitmap(mArrowLongLeft, mArrowMatrix, mPaint); |
| break; |
| case RIGHT_HANDLE_GRABBED: |
| mArrowMatrix.setTranslate(0, 0); |
| if (!isHoriz()) { |
| mArrowMatrix.preRotate(-90, 0, 0); |
| // since bg width is > height of screen in landscape mode... |
| mArrowMatrix.postTranslate(0, height + (mBackgroundWidth - height)); |
| } |
| canvas.drawBitmap(mArrowLongRight, mArrowMatrix, mPaint); |
| break; |
| default: |
| throw new IllegalStateException("invalid mGrabbedState: " + mGrabbedState); |
| } |
| |
| final int bgHeight = mBackgroundHeight; |
| final int bgTop = isHoriz() ? |
| height - bgHeight: |
| width - bgHeight; |
| |
| if (VISUAL_DEBUG) { |
| // draw circle bounding arc drawable: good sanity check we're doing the math correctly |
| float or = OUTER_ROTARY_RADIUS_DIP * mDensity; |
| final int vOffset = mBackgroundWidth - height; |
| final int midX = isHoriz() ? width / 2 : mBackgroundWidth / 2 - vOffset; |
| if (isHoriz()) { |
| canvas.drawCircle(midX, or + bgTop, or, mPaint); |
| } else { |
| canvas.drawCircle(or + bgTop, midX, or, mPaint); |
| } |
| } |
| |
| // left dimple / icon |
| { |
| final int xOffset = mLeftHandleX + mRotaryOffsetX; |
| final int drawableY = getYOnArc( |
| mBackgroundWidth, |
| mInnerRadius, |
| mOuterRadius, |
| xOffset); |
| final int x = isHoriz() ? xOffset : drawableY + bgTop; |
| final int y = isHoriz() ? drawableY + bgTop : height - xOffset; |
| if (mGrabbedState != RIGHT_HANDLE_GRABBED) { |
| drawCentered(mDimple, canvas, x, y); |
| drawCentered(mLeftHandleIcon, canvas, x, y); |
| } else { |
| drawCentered(mDimpleDim, canvas, x, y); |
| } |
| } |
| |
| // center dimple |
| { |
| final int xOffset = isHoriz() ? |
| width / 2 + mRotaryOffsetX: |
| height / 2 + mRotaryOffsetX; |
| final int drawableY = getYOnArc( |
| mBackgroundWidth, |
| mInnerRadius, |
| mOuterRadius, |
| xOffset); |
| |
| if (isHoriz()) { |
| drawCentered(mDimpleDim, canvas, xOffset, drawableY + bgTop); |
| } else { |
| // vertical |
| drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - xOffset); |
| } |
| } |
| |
| // right dimple / icon |
| { |
| final int xOffset = mRightHandleX + mRotaryOffsetX; |
| final int drawableY = getYOnArc( |
| mBackgroundWidth, |
| mInnerRadius, |
| mOuterRadius, |
| xOffset); |
| |
| final int x = isHoriz() ? xOffset : drawableY + bgTop; |
| final int y = isHoriz() ? drawableY + bgTop : height - xOffset; |
| if (mGrabbedState != LEFT_HANDLE_GRABBED) { |
| drawCentered(mDimple, canvas, x, y); |
| drawCentered(mRightHandleIcon, canvas, x, y); |
| } else { |
| drawCentered(mDimpleDim, canvas, x, y); |
| } |
| } |
| |
| // draw extra left hand dimples |
| int dimpleLeft = mRotaryOffsetX + mLeftHandleX - mDimpleSpacing; |
| final int halfdimple = mDimpleWidth / 2; |
| while (dimpleLeft > -halfdimple) { |
| final int drawableY = getYOnArc( |
| mBackgroundWidth, |
| mInnerRadius, |
| mOuterRadius, |
| dimpleLeft); |
| |
| if (isHoriz()) { |
| drawCentered(mDimpleDim, canvas, dimpleLeft, drawableY + bgTop); |
| } else { |
| drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleLeft); |
| } |
| dimpleLeft -= mDimpleSpacing; |
| } |
| |
| // draw extra right hand dimples |
| int dimpleRight = mRotaryOffsetX + mRightHandleX + mDimpleSpacing; |
| final int rightThresh = mRight + halfdimple; |
| while (dimpleRight < rightThresh) { |
| final int drawableY = getYOnArc( |
| mBackgroundWidth, |
| mInnerRadius, |
| mOuterRadius, |
| dimpleRight); |
| |
| if (isHoriz()) { |
| drawCentered(mDimpleDim, canvas, dimpleRight, drawableY + bgTop); |
| } else { |
| drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleRight); |
| } |
| dimpleRight += mDimpleSpacing; |
| } |
| } |
| |
| /** |
| * Assuming bitmap is a bounding box around a piece of an arc drawn by two concentric circles |
| * (as the background drawable for the rotary widget is), and given an x coordinate along the |
| * drawable, return the y coordinate of a point on the arc that is between the two concentric |
| * circles. The resulting y combined with the incoming x is a point along the circle in |
| * between the two concentric circles. |
| * |
| * @param backgroundWidth The width of the asset (the bottom of the box surrounding the arc). |
| * @param innerRadius The radius of the circle that intersects the drawable at the bottom two |
| * corders of the drawable (top two corners in terms of drawing coordinates). |
| * @param outerRadius The radius of the circle who's top most point is the top center of the |
| * drawable (bottom center in terms of drawing coordinates). |
| * @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 |
| * in between the two concentric circles. |
| */ |
| private int getYOnArc(int backgroundWidth, int innerRadius, int outerRadius, int x) { |
| |
| // the hypotenuse |
| final int halfWidth = (outerRadius - innerRadius) / 2; |
| final int middleRadius = innerRadius + halfWidth; |
| |
| // the bottom leg of the triangle |
| final int triangleBottom = (backgroundWidth / 2) - x; |
| |
| // "Our offense is like the pythagorean theorem: There is no answer!" - Shaquille O'Neal |
| final int triangleY = |
| (int) Math.sqrt(middleRadius * middleRadius - triangleBottom * triangleBottom); |
| |
| // convert to drawing coordinates: |
| // middleRadius - triangleY = |
| // the vertical distance from the outer edge of the circle to the desired point |
| // from there we add the distance from the top of the drawable to the middle circle |
| return middleRadius - triangleY + halfWidth; |
| } |
| |
| /** |
| * Handle touch screen events. |
| * |
| * @param event The motion event. |
| * @return True if the event was handled, false otherwise. |
| */ |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| if (mAnimating) { |
| return true; |
| } |
| if (mVelocityTracker == null) { |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| mVelocityTracker.addMovement(event); |
| |
| final int height = getHeight(); |
| |
| final int eventX = isHoriz() ? |
| (int) event.getX(): |
| height - ((int) event.getY()); |
| final int hitWindow = mDimpleWidth; |
| |
| final int action = event.getAction(); |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| if (DBG) log("touch-down"); |
| mTriggered = false; |
| if (mGrabbedState != NOTHING_GRABBED) { |
| reset(); |
| invalidate(); |
| } |
| if (eventX < mLeftHandleX + hitWindow) { |
| mRotaryOffsetX = eventX - mLeftHandleX; |
| setGrabbedState(LEFT_HANDLE_GRABBED); |
| invalidate(); |
| vibrate(VIBRATE_SHORT); |
| } else if (eventX > mRightHandleX - hitWindow) { |
| mRotaryOffsetX = eventX - mRightHandleX; |
| setGrabbedState(RIGHT_HANDLE_GRABBED); |
| invalidate(); |
| vibrate(VIBRATE_SHORT); |
| } |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| if (DBG) log("touch-move"); |
| if (mGrabbedState == LEFT_HANDLE_GRABBED) { |
| mRotaryOffsetX = eventX - mLeftHandleX; |
| invalidate(); |
| final int rightThresh = isHoriz() ? getRight() : height; |
| if (eventX >= rightThresh - mEdgeTriggerThresh && !mTriggered) { |
| mTriggered = true; |
| dispatchTriggerEvent(OnDialTriggerListener.LEFT_HANDLE); |
| final VelocityTracker velocityTracker = mVelocityTracker; |
| velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); |
| final int rawVelocity = isHoriz() ? |
| (int) velocityTracker.getXVelocity(): |
| -(int) velocityTracker.getYVelocity(); |
| final int velocity = Math.max(mMinimumVelocity, rawVelocity); |
| mDimplesOfFling = Math.max( |
| 8, |
| Math.abs(velocity / mDimpleSpacing)); |
| startAnimationWithVelocity( |
| eventX - mLeftHandleX, |
| mDimplesOfFling * mDimpleSpacing, |
| velocity); |
| } |
| } else if (mGrabbedState == RIGHT_HANDLE_GRABBED) { |
| mRotaryOffsetX = eventX - mRightHandleX; |
| invalidate(); |
| if (eventX <= mEdgeTriggerThresh && !mTriggered) { |
| mTriggered = true; |
| dispatchTriggerEvent(OnDialTriggerListener.RIGHT_HANDLE); |
| final VelocityTracker velocityTracker = mVelocityTracker; |
| velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); |
| final int rawVelocity = isHoriz() ? |
| (int) velocityTracker.getXVelocity(): |
| - (int) velocityTracker.getYVelocity(); |
| final int velocity = Math.min(-mMinimumVelocity, rawVelocity); |
| mDimplesOfFling = Math.max( |
| 8, |
| Math.abs(velocity / mDimpleSpacing)); |
| startAnimationWithVelocity( |
| eventX - mRightHandleX, |
| -(mDimplesOfFling * mDimpleSpacing), |
| velocity); |
| } |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| if (DBG) log("touch-up"); |
| // handle animating back to start if they didn't trigger |
| if (mGrabbedState == LEFT_HANDLE_GRABBED |
| && Math.abs(eventX - mLeftHandleX) > 5) { |
| // set up "snap back" animation |
| startAnimation(eventX - mLeftHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS); |
| } else if (mGrabbedState == RIGHT_HANDLE_GRABBED |
| && Math.abs(eventX - mRightHandleX) > 5) { |
| // set up "snap back" animation |
| startAnimation(eventX - mRightHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS); |
| } |
| mRotaryOffsetX = 0; |
| setGrabbedState(NOTHING_GRABBED); |
| invalidate(); |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); // wishin' we had generational GC |
| mVelocityTracker = null; |
| } |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| if (DBG) log("touch-cancel"); |
| reset(); |
| invalidate(); |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| } |
| break; |
| } |
| return true; |
| } |
| |
| private void startAnimation(int startX, int endX, int duration) { |
| mAnimating = true; |
| mAnimationStartTime = currentAnimationTimeMillis(); |
| mAnimationDuration = duration; |
| mAnimatingDeltaXStart = startX; |
| mAnimatingDeltaXEnd = endX; |
| setGrabbedState(NOTHING_GRABBED); |
| mDimplesOfFling = 0; |
| invalidate(); |
| } |
| |
| private void startAnimationWithVelocity(int startX, int endX, int pixelsPerSecond) { |
| mAnimating = true; |
| mAnimationStartTime = currentAnimationTimeMillis(); |
| mAnimationDuration = 1000 * (endX - startX) / pixelsPerSecond; |
| mAnimatingDeltaXStart = startX; |
| mAnimatingDeltaXEnd = endX; |
| setGrabbedState(NOTHING_GRABBED); |
| invalidate(); |
| } |
| |
| private void updateAnimation() { |
| final long millisSoFar = currentAnimationTimeMillis() - mAnimationStartTime; |
| final long millisLeft = mAnimationDuration - millisSoFar; |
| final int totalDeltaX = mAnimatingDeltaXStart - mAnimatingDeltaXEnd; |
| final boolean goingRight = totalDeltaX < 0; |
| if (DBG) log("millisleft for animating: " + millisLeft); |
| if (millisLeft <= 0) { |
| reset(); |
| return; |
| } |
| // from 0 to 1 as animation progresses |
| float interpolation = |
| mInterpolator.getInterpolation((float) millisSoFar / mAnimationDuration); |
| final int dx = (int) (totalDeltaX * (1 - interpolation)); |
| mRotaryOffsetX = mAnimatingDeltaXEnd + dx; |
| |
| // once we have gone far enough to animate the current buttons off screen, we start |
| // wrapping the offset back to the other side so that when the animation is finished, |
| // the buttons will come back into their original places. |
| if (mDimplesOfFling > 0) { |
| if (!goingRight && mRotaryOffsetX < -3 * mDimpleSpacing) { |
| // wrap around on fling left |
| mRotaryOffsetX += mDimplesOfFling * mDimpleSpacing; |
| } else if (goingRight && mRotaryOffsetX > 3 * mDimpleSpacing) { |
| // wrap around on fling right |
| mRotaryOffsetX -= mDimplesOfFling * mDimpleSpacing; |
| } |
| } |
| invalidate(); |
| } |
| |
| private void reset() { |
| mAnimating = false; |
| mRotaryOffsetX = 0; |
| mDimplesOfFling = 0; |
| setGrabbedState(NOTHING_GRABBED); |
| mTriggered = false; |
| } |
| |
| /** |
| * Triggers haptic feedback. |
| */ |
| private synchronized void vibrate(long duration) { |
| if (mVibrator == null) { |
| mVibrator = (android.os.Vibrator) |
| getContext().getSystemService(Context.VIBRATOR_SERVICE); |
| } |
| mVibrator.vibrate(duration); |
| } |
| |
| /** |
| * Draw the bitmap so that it's centered |
| * on the point (x,y), then draws it using specified canvas. |
| * TODO: is there already a utility method somewhere for this? |
| */ |
| private void drawCentered(Bitmap d, Canvas c, int x, int y) { |
| int w = d.getWidth(); |
| int h = d.getHeight(); |
| |
| c.drawBitmap(d, x - (w / 2), y - (h / 2), mPaint); |
| } |
| |
| |
| /** |
| * Registers a callback to be invoked when the dial |
| * is "triggered" by rotating it one way or the other. |
| * |
| * @param l the OnDialTriggerListener to attach to this view |
| */ |
| public void setOnDialTriggerListener(OnDialTriggerListener l) { |
| mOnDialTriggerListener = l; |
| } |
| |
| /** |
| * Dispatches a trigger event to our listener. |
| */ |
| private void dispatchTriggerEvent(int whichHandle) { |
| vibrate(VIBRATE_LONG); |
| if (mOnDialTriggerListener != null) { |
| mOnDialTriggerListener.onDialTrigger(this, whichHandle); |
| } |
| } |
| |
| /** |
| * Sets the current grabbed state, and dispatches a grabbed state change |
| * event to our listener. |
| */ |
| private void setGrabbedState(int newState) { |
| if (newState != mGrabbedState) { |
| mGrabbedState = newState; |
| if (mOnDialTriggerListener != null) { |
| mOnDialTriggerListener.onGrabbedStateChange(this, mGrabbedState); |
| } |
| } |
| } |
| |
| /** |
| * Interface definition for a callback to be invoked when the dial |
| * is "triggered" by rotating it one way or the other. |
| */ |
| public interface OnDialTriggerListener { |
| /** |
| * The dial was triggered because the user grabbed the left handle, |
| * and rotated the dial clockwise. |
| */ |
| public static final int LEFT_HANDLE = 1; |
| |
| /** |
| * The dial was triggered because the user grabbed the right handle, |
| * and rotated the dial counterclockwise. |
| */ |
| public static final int RIGHT_HANDLE = 2; |
| |
| /** |
| * Called when the dial is triggered. |
| * |
| * @param v The view that was triggered |
| * @param whichHandle Which "dial handle" the user grabbed, |
| * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}. |
| */ |
| void onDialTrigger(View v, int whichHandle); |
| |
| /** |
| * Called when the "grabbed state" changes (i.e. when |
| * the user either grabs or releases one of the handles.) |
| * |
| * @param v the view that was triggered |
| * @param grabbedState the new state: either {@link #NOTHING_GRABBED}, |
| * {@link #LEFT_HANDLE_GRABBED}, or {@link #RIGHT_HANDLE_GRABBED}. |
| */ |
| void onGrabbedStateChange(View v, int grabbedState); |
| } |
| |
| |
| // Debugging / testing code |
| |
| private void log(String msg) { |
| Log.d(LOG_TAG, msg); |
| } |
| } |