summaryrefslogtreecommitdiffstats
path: root/core/java/com/android/internal/widget/RotarySelector.java
diff options
context:
space:
mode:
authorKarl Rosaen <krosaen@android.com>2009-09-15 17:36:09 -0700
committerKarl Rosaen <krosaen@android.com>2009-09-15 17:46:57 -0700
commite4d95d02a25fb6596a3bf622ba57d4145773da90 (patch)
tree92ec70e220f88532697c3468db91fc07b143c3b2 /core/java/com/android/internal/widget/RotarySelector.java
parent5446ea77bcd7cf3d234f7198d073c99f6519662f (diff)
downloadframeworks_base-e4d95d02a25fb6596a3bf622ba57d4145773da90.zip
frameworks_base-e4d95d02a25fb6596a3bf622ba57d4145773da90.tar.gz
frameworks_base-e4d95d02a25fb6596a3bf622ba57d4145773da90.tar.bz2
Add RotarySelector widget to android.internal for use by lock screen and incoming call screen.
Diffstat (limited to 'core/java/com/android/internal/widget/RotarySelector.java')
-rw-r--r--core/java/com/android/internal/widget/RotarySelector.java542
1 files changed, 542 insertions, 0 deletions
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);
+ }
+}