From e4d95d02a25fb6596a3bf622ba57d4145773da90 Mon Sep 17 00:00:00 2001 From: Karl Rosaen Date: Tue, 15 Sep 2009 17:36:09 -0700 Subject: Add RotarySelector widget to android.internal for use by lock screen and incoming call screen. --- .../android/internal/widget/RotarySelector.java | 542 +++++++++++++++++++++ 1 file changed, 542 insertions(+) create mode 100644 core/java/com/android/internal/widget/RotarySelector.java (limited to 'core/java/com/android/internal/widget/RotarySelector.java') diff --git a/core/java/com/android/internal/widget/RotarySelector.java b/core/java/com/android/internal/widget/RotarySelector.java new file mode 100644 index 0000000..7b940c9 --- /dev/null +++ b/core/java/com/android/internal/widget/RotarySelector.java @@ -0,0 +1,542 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Color; +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.animation.AccelerateInterpolator; + +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 { + private static final String LOG_TAG = "RotarySelector"; + private static final boolean DBG = false; + + // Listener for onDialTrigger() callbacks. + private OnDialTriggerListener mOnDialTriggerListener; + + private float mDensity; + + // UI elements + private Drawable mBackground; + private Drawable mDimple; + + private Drawable mLeftHandleIcon; + private Drawable mRightHandleIcon; + + private Drawable mArrowShortLeftAndRight; + private Drawable mArrowLongLeft; // Long arrow starting on the left, pointing clockwise + private Drawable 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 user's dragging + private int mTouchDragOffset = 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 mAnimationEndTime; + private int mAnimatingDelta; + AccelerateInterpolator mInterpolator; + + /** + * True after triggering an action if the user of {@link OnDialTriggerListener} wants to + * freeze the UI (until they transition to another screen). + */ + private boolean mFrozen = false; + + /** + * If the user is currently dragging something. + */ + private int mGrabbedState = NOTHING_GRABBED; + private static final int NOTHING_GRABBED = 0; + private static final int LEFT_HANDLE_GRABBED = 1; + private 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 = 60; // msec + private static final long VIBRATE_LONG = 100; // msec + + // Various tweakable layout or behavior parameters: + + // How close to the edge of the screen, we let the handle get before + // triggering an action: + private static final int EDGE_THRESHOLD_DIP = 70; + + /** + * 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; + + /** + * Dimensions of arc in background drawable. + */ + static final int OUTER_ROTARY_RADIUS_DIP = 390; + static final int ROTARY_STROKE_WIDTH_DIP = 83; + private static final int ANIMATION_DURATION_MILLIS = 300; + + private static final boolean DRAW_CENTER_DIMPLE = false; + + /** + * Constructor used when this widget is created from a layout file. + */ + public RotarySelector(Context context, AttributeSet attrs) { + super(context, attrs); + if (DBG) log("IncomingCallDialWidget constructor..."); + + Resources r = getResources(); + mDensity = r.getDisplayMetrics().density; + if (DBG) log("- Density: " + mDensity); + // Density is 1.0 on HVGA (like Dream), and 1.5 on WVGA. + // Usage: raw_pixel_value = (int) (dpi_value * mDensity + 0.5f) + + // Assets (all are BitmapDrawables). + mBackground = r.getDrawable(R.drawable.jog_dial_bg_cropped); + mDimple = r.getDrawable(R.drawable.jog_dial_dimple); + + mArrowLongLeft = r.getDrawable(R.drawable.jog_dial_arrow_long_left_green); + mArrowLongRight = r.getDrawable(R.drawable.jog_dial_arrow_long_right_red); + mArrowShortLeftAndRight = r.getDrawable(R.drawable.jog_dial_arrow_short_left_and_right); + + mInterpolator = new AccelerateInterpolator(); + } + + /** + * 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) { + Drawable d = null; + if (resId != 0) { + d = getResources().getDrawable(resId); + } + setLeftHandleDrawable(d); + } + + /** + * Sets the left handle icon to a given Drawable. + * + * @param d the Drawable to use as the icon, or null to remove the icon. + */ + public void setLeftHandleDrawable(Drawable d) { + mLeftHandleIcon = d; + 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) { + Drawable d = null; + if (resId != 0) { + d = getResources().getDrawable(resId); + } + setRightHandleDrawable(d); + } + + /** + * Sets the right handle icon to a given Drawable. + * + * @param d the Drawable to use as the icon, or null to remove the icon. + */ + public void setRightHandleDrawable(Drawable d) { + mRightHandleIcon = d; + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); // screen width + + final int arrowH = mArrowShortLeftAndRight.getIntrinsicHeight(); + final int backgroundH = mBackground.getIntrinsicHeight(); + + // 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 arrowScrunch = (int) (ARROW_SCRUNCH_DIP * mDensity); + setMeasuredDimension(width, backgroundH + arrowH - arrowScrunch); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + mLeftHandleX = (int) (EDGE_PADDING_DIP * mDensity) + mDimple.getIntrinsicWidth() / 2; + mRightHandleX = + getWidth() - (int) (EDGE_PADDING_DIP * mDensity) - mDimple.getIntrinsicWidth() / 2; + } + +// private Paint mPaint = new Paint(); + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (DBG) { + log(String.format("onDraw: mAnimating=%s, mTouchDragOffset=%d, mGrabbedState=%d," + + "mFrozen=%s", + mAnimating, mTouchDragOffset, mGrabbedState, mFrozen)); + } + + final int height = getHeight(); + + // update animating state before we draw anything + if (mAnimating && !mFrozen) { + long millisLeft = mAnimationEndTime - System.currentTimeMillis(); + if (DBG) log("millisleft for animating: " + millisLeft); + if (millisLeft <= 0) { + reset(); + } else { + float interpolation = mInterpolator.getInterpolation( + (float) millisLeft / ANIMATION_DURATION_MILLIS); + mTouchDragOffset = (int) (mAnimatingDelta * interpolation); + } + } + + + // Background: + final int backgroundW = mBackground.getIntrinsicWidth(); + final int backgroundH = mBackground.getIntrinsicHeight(); + final int backgroundY = height - backgroundH; + if (DBG) log("- Background INTRINSIC: " + backgroundW + " x " + backgroundH); + mBackground.setBounds(0, backgroundY, + backgroundW, backgroundY + backgroundH); + if (DBG) log(" Background BOUNDS: " + mBackground.getBounds()); + mBackground.draw(canvas); + + // Arrows: + // All arrow assets are the same size (they're the full width of + // the screen) regardless of which arrows are actually visible. + int arrowW = mArrowShortLeftAndRight.getIntrinsicWidth(); + int arrowH = mArrowShortLeftAndRight.getIntrinsicHeight(); + + // Draw the correct arrow(s) depending on the current state: + Drawable currentArrow; + switch (mGrabbedState) { + case NOTHING_GRABBED: + currentArrow = mArrowShortLeftAndRight; + break; + case LEFT_HANDLE_GRABBED: + currentArrow = mArrowLongLeft; + break; + case RIGHT_HANDLE_GRABBED: + currentArrow = mArrowLongRight; + break; + default: + throw new IllegalStateException("invalid mGrabbedState: " + mGrabbedState); + } + currentArrow.setBounds(0, 0, arrowW, arrowH); + currentArrow.draw(canvas); + + // debug: draw circle that should match the outer arc (good sanity check) +// mPaint.setColor(Color.RED); +// mPaint.setStyle(Paint.Style.STROKE); +// float or = OUTER_ROTARY_RADIUS_DIP * mDensity; +// canvas.drawCircle(getWidth() / 2, or + mBackground.getBounds().top, or, mPaint); + + final int outerRadius = (int) (mDensity * OUTER_ROTARY_RADIUS_DIP); + final int innerRadius = + (int) ((OUTER_ROTARY_RADIUS_DIP - ROTARY_STROKE_WIDTH_DIP) * mDensity); + final int bgTop = mBackground.getBounds().top; + { + final int xOffset = mLeftHandleX + mTouchDragOffset; + final int drawableY = getYOnArc( + mBackground, + innerRadius, + outerRadius, + xOffset); + + drawCentered(mDimple, canvas, xOffset, drawableY + bgTop); + drawCentered(mLeftHandleIcon, canvas, xOffset, drawableY + bgTop); + } + + if (DRAW_CENTER_DIMPLE) { + final int xOffset = getWidth() / 2 + mTouchDragOffset; + final int drawableY = getYOnArc( + mBackground, + innerRadius, + outerRadius, + xOffset); + + drawCentered(mDimple, canvas, xOffset, drawableY + bgTop); + } + + { + final int xOffset = mRightHandleX + mTouchDragOffset; + final int drawableY = getYOnArc( + mBackground, + innerRadius, + outerRadius, + xOffset); + + drawCentered(mDimple, canvas, xOffset, drawableY + bgTop); + drawCentered(mRightHandleIcon, canvas, xOffset, drawableY + bgTop); + } + + if (mAnimating) invalidate(); + } + + /** + * Assuming drawable 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 drawable The drawable. + * @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(Drawable drawable, 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 = (drawable.getIntrinsicWidth() / 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 || mFrozen) { + return true; + } + + final int eventX = (int) event.getX(); + final int hitWindow = mDimple.getIntrinsicWidth(); + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (DBG) log("touch-down"); + mTriggered = false; + if (mGrabbedState != RotarySelector.NOTHING_GRABBED) { + reset(); + invalidate(); + } + if (eventX < mLeftHandleX + hitWindow) { + mTouchDragOffset = eventX - mLeftHandleX; + mGrabbedState = RotarySelector.LEFT_HANDLE_GRABBED; + invalidate(); + vibrate(VIBRATE_SHORT); + } else if (eventX > mRightHandleX - hitWindow) { + mTouchDragOffset = eventX - mRightHandleX; + mGrabbedState = RotarySelector.RIGHT_HANDLE_GRABBED; + invalidate(); + vibrate(VIBRATE_SHORT); + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (DBG) log("touch-move"); + if (mGrabbedState == RotarySelector.LEFT_HANDLE_GRABBED) { + mTouchDragOffset = eventX - mLeftHandleX; + invalidate(); + if (eventX >= mRightHandleX - EDGE_PADDING_DIP && !mTriggered) { + mTriggered = true; + mFrozen = dispatchTriggerEvent(OnDialTriggerListener.LEFT_HANDLE); + } + } else if (mGrabbedState == RotarySelector.RIGHT_HANDLE_GRABBED) { + mTouchDragOffset = eventX - mRightHandleX; + invalidate(); + if (eventX <= mLeftHandleX + EDGE_PADDING_DIP && !mTriggered) { + mTriggered = true; + mFrozen = dispatchTriggerEvent(OnDialTriggerListener.RIGHT_HANDLE); + } + } + } else if ((event.getAction() == MotionEvent.ACTION_UP)) { + if (DBG) log("touch-up"); + // handle animating back to start if they didn't trigger + if (mGrabbedState == RotarySelector.LEFT_HANDLE_GRABBED + && Math.abs(eventX - mLeftHandleX) > 5) { + mAnimating = true; + mAnimationEndTime = System.currentTimeMillis() + ANIMATION_DURATION_MILLIS; + mAnimatingDelta = eventX - mLeftHandleX; + } else if (mGrabbedState == RotarySelector.RIGHT_HANDLE_GRABBED + && Math.abs(eventX - mRightHandleX) > 5) { + mAnimating = true; + mAnimationEndTime = System.currentTimeMillis() + ANIMATION_DURATION_MILLIS; + mAnimatingDelta = eventX - mRightHandleX; + } + + mTouchDragOffset = 0; + mGrabbedState = RotarySelector.NOTHING_GRABBED; + invalidate(); + } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { + if (DBG) log("touch-cancel"); + reset(); + invalidate(); + } + return true; + } + + private void reset() { + mAnimating = false; + mTouchDragOffset = 0; + mGrabbedState = RotarySelector.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); + } + + /** + * Sets the bounds of the specified Drawable so that it's centered + * on the point (x,y), then draws it onto the specified canvas. + * TODO: is there already a utility method somewhere for this? + */ + private static void drawCentered(Drawable d, Canvas c, int x, int y) { + int w = d.getIntrinsicWidth(); + int h = d.getIntrinsicHeight(); + + // if (DBG) log("--> drawCentered: " + x + " , " + y + "; intrinsic " + w + " x " + h); + d.setBounds(x - (w / 2), y - (h / 2), + x + (w / 2), y + (h / 2)); + d.draw(c); + } + + + /** + * 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 boolean dispatchTriggerEvent(int whichHandle) { + vibrate(VIBRATE_LONG); + if (mOnDialTriggerListener != null) { + return mOnDialTriggerListener.onDialTrigger(this, whichHandle); + } + return false; + } + + /** + * 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; + + /** + * @hide + * The center handle is currently unused. + */ + public static final int CENTER_HANDLE = 3; + + /** + * 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}, or + * {@link #CENTER_HANDLE}. + * @return Whether the widget should freeze (e.g when the action goes to another screen, + * you want the UI to stay put until the transition occurs). + */ + boolean onDialTrigger(View v, int whichHandle); + } + + + // Debugging / testing code + + private void log(String msg) { + Log.d(LOG_TAG, msg); + } +} -- cgit v1.1