blob: 4e405f4cd7e82c4fbd320140a654a1ffd8c28b30 [file] [log] [blame]
Karl Rosaene4d95d02009-09-15 17:36:09 -07001/*
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
17package com.android.internal.widget;
18
19import android.content.Context;
20import android.content.res.Resources;
Karl Rosaen74646ad2009-09-23 17:00:55 -070021import android.content.res.TypedArray;
Karl Rosaene4d95d02009-09-15 17:36:09 -070022import android.graphics.Canvas;
Karl Rosaen74646ad2009-09-23 17:00:55 -070023import android.graphics.Paint;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Matrix;
Karl Rosaene4d95d02009-09-15 17:36:09 -070027import android.graphics.drawable.Drawable;
Jeff Sharkey723a7252012-10-12 14:26:31 -070028import android.os.UserHandle;
Karl Rosaene4d95d02009-09-15 17:36:09 -070029import android.os.Vibrator;
Jeff Sharkey723a7252012-10-12 14:26:31 -070030import android.provider.Settings;
Karl Rosaene4d95d02009-09-15 17:36:09 -070031import android.util.AttributeSet;
32import android.util.Log;
33import android.view.MotionEvent;
34import android.view.View;
Karl Rosaen896264f2009-09-22 11:36:23 -070035import android.view.VelocityTracker;
36import android.view.ViewConfiguration;
37import android.view.animation.DecelerateInterpolator;
Karl Rosaen1ca654e2009-09-16 10:51:10 -070038import static android.view.animation.AnimationUtils.currentAnimationTimeMillis;
Karl Rosaene4d95d02009-09-15 17:36:09 -070039import com.android.internal.R;
40
41
42/**
43 * Custom view that presents up to two items that are selectable by rotating a semi-circle from
44 * left to right, or right to left. Used by incoming call screen, and the lock screen when no
45 * security pattern is set.
46 */
47public class RotarySelector extends View {
Karl Rosaen74646ad2009-09-23 17:00:55 -070048 public static final int HORIZONTAL = 0;
49 public static final int VERTICAL = 1;
50
Karl Rosaene4d95d02009-09-15 17:36:09 -070051 private static final String LOG_TAG = "RotarySelector";
52 private static final boolean DBG = false;
Jim Miller5037e572009-10-05 13:00:58 -070053 private static final boolean VISUAL_DEBUG = false;
Karl Rosaene4d95d02009-09-15 17:36:09 -070054
55 // Listener for onDialTrigger() callbacks.
56 private OnDialTriggerListener mOnDialTriggerListener;
57
58 private float mDensity;
59
60 // UI elements
Karl Rosaen74646ad2009-09-23 17:00:55 -070061 private Bitmap mBackground;
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -070062 private Bitmap mDimple;
Jim Millerd9b6f142009-09-30 22:50:01 -070063 private Bitmap mDimpleDim;
Karl Rosaene4d95d02009-09-15 17:36:09 -070064
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -070065 private Bitmap mLeftHandleIcon;
66 private Bitmap mRightHandleIcon;
Karl Rosaene4d95d02009-09-15 17:36:09 -070067
Karl Rosaen74646ad2009-09-23 17:00:55 -070068 private Bitmap mArrowShortLeftAndRight;
69 private Bitmap mArrowLongLeft; // Long arrow starting on the left, pointing clockwise
70 private Bitmap mArrowLongRight; // Long arrow starting on the right, pointing CCW
Karl Rosaene4d95d02009-09-15 17:36:09 -070071
72 // positions of the left and right handle
73 private int mLeftHandleX;
74 private int mRightHandleX;
75
Karl Rosaen896264f2009-09-22 11:36:23 -070076 // current offset of rotary widget along the x axis
77 private int mRotaryOffsetX = 0;
Karl Rosaene4d95d02009-09-15 17:36:09 -070078
79 // state of the animation used to bring the handle back to its start position when
80 // the user lets go before triggering an action
81 private boolean mAnimating = false;
Karl Rosaen896264f2009-09-22 11:36:23 -070082 private long mAnimationStartTime;
Karl Rosaen052e1872009-09-20 15:03:20 -070083 private long mAnimationDuration;
Karl Rosaen896264f2009-09-22 11:36:23 -070084 private int mAnimatingDeltaXStart; // the animation will interpolate from this delta to zero
85 private int mAnimatingDeltaXEnd;
86
87 private DecelerateInterpolator mInterpolator;
Karl Rosaene4d95d02009-09-15 17:36:09 -070088
Karl Rosaen74646ad2009-09-23 17:00:55 -070089 private Paint mPaint = new Paint();
90
91 // used to rotate the background and arrow assets depending on orientation
92 final Matrix mBgMatrix = new Matrix();
93 final Matrix mArrowMatrix = new Matrix();
94
Karl Rosaene4d95d02009-09-15 17:36:09 -070095 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -070096 * If the user is currently dragging something.
97 */
98 private int mGrabbedState = NOTHING_GRABBED;
David Brown88e03752009-10-01 19:25:54 -070099 public static final int NOTHING_GRABBED = 0;
100 public static final int LEFT_HANDLE_GRABBED = 1;
101 public static final int RIGHT_HANDLE_GRABBED = 2;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700102
103 /**
104 * Whether the user has triggered something (e.g dragging the left handle all the way over to
105 * the right).
106 */
107 private boolean mTriggered = false;
108
109 // Vibration (haptic feedback)
110 private Vibrator mVibrator;
Jim Miller9485aec2009-10-08 18:49:53 -0700111 private static final long VIBRATE_SHORT = 20; // msec
112 private static final long VIBRATE_LONG = 20; // msec
Karl Rosaene4d95d02009-09-15 17:36:09 -0700113
Karl Rosaene4d95d02009-09-15 17:36:09 -0700114 /**
115 * The drawable for the arrows need to be scrunched this many dips towards the rotary bg below
116 * it.
117 */
118 private static final int ARROW_SCRUNCH_DIP = 6;
119
120 /**
121 * How far inset the left and right circles should be
122 */
123 private static final int EDGE_PADDING_DIP = 9;
124
125 /**
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700126 * How far from the edge of the screen the user must drag to trigger the event.
127 */
Karl Rosaen052e1872009-09-20 15:03:20 -0700128 private static final int EDGE_TRIGGER_DIP = 100;
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700129
130 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700131 * Dimensions of arc in background drawable.
132 */
133 static final int OUTER_ROTARY_RADIUS_DIP = 390;
134 static final int ROTARY_STROKE_WIDTH_DIP = 83;
Karl Rosaen052e1872009-09-20 15:03:20 -0700135 static final int SNAP_BACK_ANIMATION_DURATION_MILLIS = 300;
136 static final int SPIN_ANIMATION_DURATION_MILLIS = 800;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700137
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700138 private int mEdgeTriggerThresh;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700139 private int mDimpleWidth;
140 private int mBackgroundWidth;
141 private int mBackgroundHeight;
Karl Rosaen896264f2009-09-22 11:36:23 -0700142 private final int mOuterRadius;
143 private final int mInnerRadius;
144 private int mDimpleSpacing;
145
146 private VelocityTracker mVelocityTracker;
147 private int mMinimumVelocity;
148 private int mMaximumVelocity;
149
150 /**
151 * The number of dimples we are flinging when we do the "spin" animation. Used to know when to
152 * wrap the icons back around so they "rotate back" onto the screen.
153 * @see #updateAnimation()
154 */
155 private int mDimplesOfFling = 0;
156
Karl Rosaen74646ad2009-09-23 17:00:55 -0700157 /**
158 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
159 */
160 private int mOrientation;
Karl Rosaen896264f2009-09-22 11:36:23 -0700161
Karl Rosaene4d95d02009-09-15 17:36:09 -0700162
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700163 public RotarySelector(Context context) {
164 this(context, null);
165 }
166
Karl Rosaene4d95d02009-09-15 17:36:09 -0700167 /**
168 * Constructor used when this widget is created from a layout file.
169 */
170 public RotarySelector(Context context, AttributeSet attrs) {
171 super(context, attrs);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700172
173 TypedArray a =
174 context.obtainStyledAttributes(attrs, R.styleable.RotarySelector);
175 mOrientation = a.getInt(R.styleable.RotarySelector_orientation, HORIZONTAL);
176 a.recycle();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700177
178 Resources r = getResources();
179 mDensity = r.getDisplayMetrics().density;
180 if (DBG) log("- Density: " + mDensity);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700181
182 // Assets (all are BitmapDrawables).
Karl Rosaen74646ad2009-09-23 17:00:55 -0700183 mBackground = getBitmapFor(R.drawable.jog_dial_bg);
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700184 mDimple = getBitmapFor(R.drawable.jog_dial_dimple);
Jim Millerd9b6f142009-09-30 22:50:01 -0700185 mDimpleDim = getBitmapFor(R.drawable.jog_dial_dimple_dim);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700186
Karl Rosaen74646ad2009-09-23 17:00:55 -0700187 mArrowLongLeft = getBitmapFor(R.drawable.jog_dial_arrow_long_left_green);
188 mArrowLongRight = getBitmapFor(R.drawable.jog_dial_arrow_long_right_red);
189 mArrowShortLeftAndRight = getBitmapFor(R.drawable.jog_dial_arrow_short_left_and_right);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700190
Karl Rosaen896264f2009-09-22 11:36:23 -0700191 mInterpolator = new DecelerateInterpolator(1f);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700192
193 mEdgeTriggerThresh = (int) (mDensity * EDGE_TRIGGER_DIP);
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700194
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700195 mDimpleWidth = mDimple.getWidth();
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700196
Karl Rosaen74646ad2009-09-23 17:00:55 -0700197 mBackgroundWidth = mBackground.getWidth();
198 mBackgroundHeight = mBackground.getHeight();
Karl Rosaen896264f2009-09-22 11:36:23 -0700199 mOuterRadius = (int) (mDensity * OUTER_ROTARY_RADIUS_DIP);
200 mInnerRadius = (int) ((OUTER_ROTARY_RADIUS_DIP - ROTARY_STROKE_WIDTH_DIP) * mDensity);
201
202 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
203 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity() * 2;
204 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
205 }
206
Karl Rosaen74646ad2009-09-23 17:00:55 -0700207 private Bitmap getBitmapFor(int resId) {
208 return BitmapFactory.decodeResource(getContext().getResources(), resId);
209 }
210
Karl Rosaen896264f2009-09-22 11:36:23 -0700211 @Override
212 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
213 super.onSizeChanged(w, h, oldw, oldh);
214
Karl Rosaen74646ad2009-09-23 17:00:55 -0700215 final int edgePadding = (int) (EDGE_PADDING_DIP * mDensity);
216 mLeftHandleX = edgePadding + mDimpleWidth / 2;
217 final int length = isHoriz() ? w : h;
218 mRightHandleX = length - edgePadding - mDimpleWidth / 2;
219 mDimpleSpacing = (length / 2) - mLeftHandleX;
Karl Rosaen896264f2009-09-22 11:36:23 -0700220
Karl Rosaen74646ad2009-09-23 17:00:55 -0700221 // bg matrix only needs to be calculated once
222 mBgMatrix.setTranslate(0, 0);
223 if (!isHoriz()) {
224 // set up matrix for translating drawing of background and arrow assets
225 final int left = w - mBackgroundHeight;
226 mBgMatrix.preRotate(-90, 0, 0);
227 mBgMatrix.postTranslate(left, h);
228
229 } else {
230 mBgMatrix.postTranslate(0, h - mBackgroundHeight);
231 }
232 }
233
234 private boolean isHoriz() {
235 return mOrientation == HORIZONTAL;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700236 }
237
238 /**
239 * Sets the left handle icon to a given resource.
240 *
241 * The resource should refer to a Drawable object, or use 0 to remove
242 * the icon.
243 *
244 * @param resId the resource ID.
245 */
246 public void setLeftHandleResource(int resId) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700247 if (resId != 0) {
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700248 mLeftHandleIcon = getBitmapFor(resId);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700249 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700250 invalidate();
251 }
252
253 /**
254 * Sets the right handle icon to a given resource.
255 *
256 * The resource should refer to a Drawable object, or use 0 to remove
257 * the icon.
258 *
259 * @param resId the resource ID.
260 */
261 public void setRightHandleResource(int resId) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700262 if (resId != 0) {
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700263 mRightHandleIcon = getBitmapFor(resId);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700264 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700265 invalidate();
266 }
267
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700268
Karl Rosaene4d95d02009-09-15 17:36:09 -0700269 @Override
270 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Karl Rosaen74646ad2009-09-23 17:00:55 -0700271 final int length = isHoriz() ?
272 MeasureSpec.getSize(widthMeasureSpec) :
273 MeasureSpec.getSize(heightMeasureSpec);
274 final int arrowScrunch = (int) (ARROW_SCRUNCH_DIP * mDensity);
275 final int arrowH = mArrowShortLeftAndRight.getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700276
277 // by making the height less than arrow + bg, arrow and bg will be scrunched together,
278 // overlaying somewhat (though on transparent portions of the drawable).
279 // this works because the arrows are drawn from the top, and the rotary bg is drawn
280 // from the bottom.
Karl Rosaen74646ad2009-09-23 17:00:55 -0700281 final int height = mBackgroundHeight + arrowH - arrowScrunch;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700282
Karl Rosaen74646ad2009-09-23 17:00:55 -0700283 if (isHoriz()) {
284 setMeasuredDimension(length, height);
285 } else {
286 setMeasuredDimension(height, length);
287 }
288 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700289
290 @Override
291 protected void onDraw(Canvas canvas) {
292 super.onDraw(canvas);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700293
294 final int width = getWidth();
295
Jim Miller5037e572009-10-05 13:00:58 -0700296 if (VISUAL_DEBUG) {
297 // draw bounding box around widget
298 mPaint.setColor(0xffff0000);
299 mPaint.setStyle(Paint.Style.STROKE);
300 canvas.drawRect(0, 0, width, getHeight(), mPaint);
301 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700302
303 final int height = getHeight();
304
305 // update animating state before we draw anything
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700306 if (mAnimating) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700307 updateAnimation();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700308 }
309
Karl Rosaene4d95d02009-09-15 17:36:09 -0700310 // Background:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700311 canvas.drawBitmap(mBackground, mBgMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700312
313 // Draw the correct arrow(s) depending on the current state:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700314 mArrowMatrix.reset();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700315 switch (mGrabbedState) {
316 case NOTHING_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700317 //mArrowShortLeftAndRight;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700318 break;
319 case LEFT_HANDLE_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700320 mArrowMatrix.setTranslate(0, 0);
321 if (!isHoriz()) {
322 mArrowMatrix.preRotate(-90, 0, 0);
323 mArrowMatrix.postTranslate(0, height);
324 }
325 canvas.drawBitmap(mArrowLongLeft, mArrowMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700326 break;
327 case RIGHT_HANDLE_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700328 mArrowMatrix.setTranslate(0, 0);
329 if (!isHoriz()) {
330 mArrowMatrix.preRotate(-90, 0, 0);
331 // since bg width is > height of screen in landscape mode...
332 mArrowMatrix.postTranslate(0, height + (mBackgroundWidth - height));
333 }
334 canvas.drawBitmap(mArrowLongRight, mArrowMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700335 break;
336 default:
337 throw new IllegalStateException("invalid mGrabbedState: " + mGrabbedState);
338 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700339
Karl Rosaen74646ad2009-09-23 17:00:55 -0700340 final int bgHeight = mBackgroundHeight;
341 final int bgTop = isHoriz() ?
342 height - bgHeight:
343 width - bgHeight;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700344
Jim Miller5037e572009-10-05 13:00:58 -0700345 if (VISUAL_DEBUG) {
346 // draw circle bounding arc drawable: good sanity check we're doing the math correctly
347 float or = OUTER_ROTARY_RADIUS_DIP * mDensity;
348 final int vOffset = mBackgroundWidth - height;
349 final int midX = isHoriz() ? width / 2 : mBackgroundWidth / 2 - vOffset;
350 if (isHoriz()) {
351 canvas.drawCircle(midX, or + bgTop, or, mPaint);
352 } else {
353 canvas.drawCircle(or + bgTop, midX, or, mPaint);
354 }
355 }
Jim Millerd9b6f142009-09-30 22:50:01 -0700356
Karl Rosaen74646ad2009-09-23 17:00:55 -0700357 // left dimple / icon
Karl Rosaene4d95d02009-09-15 17:36:09 -0700358 {
Karl Rosaen896264f2009-09-22 11:36:23 -0700359 final int xOffset = mLeftHandleX + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700360 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700361 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700362 mInnerRadius,
363 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700364 xOffset);
Jim Miller5037e572009-10-05 13:00:58 -0700365 final int x = isHoriz() ? xOffset : drawableY + bgTop;
366 final int y = isHoriz() ? drawableY + bgTop : height - xOffset;
367 if (mGrabbedState != RIGHT_HANDLE_GRABBED) {
368 drawCentered(mDimple, canvas, x, y);
369 drawCentered(mLeftHandleIcon, canvas, x, y);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700370 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700371 drawCentered(mDimpleDim, canvas, x, y);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700372 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700373 }
374
Karl Rosaen74646ad2009-09-23 17:00:55 -0700375 // center dimple
376 {
377 final int xOffset = isHoriz() ?
378 width / 2 + mRotaryOffsetX:
379 height / 2 + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700380 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700381 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700382 mInnerRadius,
383 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700384 xOffset);
385
Karl Rosaen74646ad2009-09-23 17:00:55 -0700386 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700387 drawCentered(mDimpleDim, canvas, xOffset, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700388 } else {
389 // vertical
Jim Miller5037e572009-10-05 13:00:58 -0700390 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - xOffset);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700391 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700392 }
393
Karl Rosaen74646ad2009-09-23 17:00:55 -0700394 // right dimple / icon
Karl Rosaene4d95d02009-09-15 17:36:09 -0700395 {
Karl Rosaen896264f2009-09-22 11:36:23 -0700396 final int xOffset = mRightHandleX + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700397 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700398 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700399 mInnerRadius,
400 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700401 xOffset);
402
Jim Miller5037e572009-10-05 13:00:58 -0700403 final int x = isHoriz() ? xOffset : drawableY + bgTop;
404 final int y = isHoriz() ? drawableY + bgTop : height - xOffset;
405 if (mGrabbedState != LEFT_HANDLE_GRABBED) {
406 drawCentered(mDimple, canvas, x, y);
407 drawCentered(mRightHandleIcon, canvas, x, y);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700408 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700409 drawCentered(mDimpleDim, canvas, x, y);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700410 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700411 }
412
Karl Rosaen896264f2009-09-22 11:36:23 -0700413 // draw extra left hand dimples
414 int dimpleLeft = mRotaryOffsetX + mLeftHandleX - mDimpleSpacing;
415 final int halfdimple = mDimpleWidth / 2;
416 while (dimpleLeft > -halfdimple) {
417 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700418 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700419 mInnerRadius,
420 mOuterRadius,
421 dimpleLeft);
422
Karl Rosaen74646ad2009-09-23 17:00:55 -0700423 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700424 drawCentered(mDimpleDim, canvas, dimpleLeft, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700425 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700426 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleLeft);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700427 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700428 dimpleLeft -= mDimpleSpacing;
429 }
430
431 // draw extra right hand dimples
432 int dimpleRight = mRotaryOffsetX + mRightHandleX + mDimpleSpacing;
433 final int rightThresh = mRight + halfdimple;
434 while (dimpleRight < rightThresh) {
435 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700436 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700437 mInnerRadius,
438 mOuterRadius,
439 dimpleRight);
440
Karl Rosaen74646ad2009-09-23 17:00:55 -0700441 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700442 drawCentered(mDimpleDim, canvas, dimpleRight, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700443 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700444 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleRight);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700445 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700446 dimpleRight += mDimpleSpacing;
447 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700448 }
449
450 /**
Karl Rosaen74646ad2009-09-23 17:00:55 -0700451 * Assuming bitmap is a bounding box around a piece of an arc drawn by two concentric circles
Karl Rosaene4d95d02009-09-15 17:36:09 -0700452 * (as the background drawable for the rotary widget is), and given an x coordinate along the
453 * drawable, return the y coordinate of a point on the arc that is between the two concentric
454 * circles. The resulting y combined with the incoming x is a point along the circle in
455 * between the two concentric circles.
456 *
Karl Rosaen74646ad2009-09-23 17:00:55 -0700457 * @param backgroundWidth The width of the asset (the bottom of the box surrounding the arc).
Karl Rosaene4d95d02009-09-15 17:36:09 -0700458 * @param innerRadius The radius of the circle that intersects the drawable at the bottom two
459 * corders of the drawable (top two corners in terms of drawing coordinates).
460 * @param outerRadius The radius of the circle who's top most point is the top center of the
461 * drawable (bottom center in terms of drawing coordinates).
Karl Rosaen74646ad2009-09-23 17:00:55 -0700462 * @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
Karl Rosaene4d95d02009-09-15 17:36:09 -0700463 * in between the two concentric circles.
464 */
Karl Rosaen74646ad2009-09-23 17:00:55 -0700465 private int getYOnArc(int backgroundWidth, int innerRadius, int outerRadius, int x) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700466
467 // the hypotenuse
468 final int halfWidth = (outerRadius - innerRadius) / 2;
469 final int middleRadius = innerRadius + halfWidth;
470
471 // the bottom leg of the triangle
Karl Rosaen74646ad2009-09-23 17:00:55 -0700472 final int triangleBottom = (backgroundWidth / 2) - x;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700473
474 // "Our offense is like the pythagorean theorem: There is no answer!" - Shaquille O'Neal
475 final int triangleY =
476 (int) Math.sqrt(middleRadius * middleRadius - triangleBottom * triangleBottom);
477
478 // convert to drawing coordinates:
479 // middleRadius - triangleY =
480 // the vertical distance from the outer edge of the circle to the desired point
481 // from there we add the distance from the top of the drawable to the middle circle
482 return middleRadius - triangleY + halfWidth;
483 }
484
485 /**
486 * Handle touch screen events.
487 *
488 * @param event The motion event.
489 * @return True if the event was handled, false otherwise.
490 */
491 @Override
492 public boolean onTouchEvent(MotionEvent event) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700493 if (mAnimating) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700494 return true;
495 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700496 if (mVelocityTracker == null) {
497 mVelocityTracker = VelocityTracker.obtain();
498 }
499 mVelocityTracker.addMovement(event);
500
Karl Rosaen74646ad2009-09-23 17:00:55 -0700501 final int height = getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700502
Karl Rosaen74646ad2009-09-23 17:00:55 -0700503 final int eventX = isHoriz() ?
504 (int) event.getX():
505 height - ((int) event.getY());
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700506 final int hitWindow = mDimpleWidth;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700507
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700508 final int action = event.getAction();
509 switch (action) {
510 case MotionEvent.ACTION_DOWN:
511 if (DBG) log("touch-down");
512 mTriggered = false;
513 if (mGrabbedState != NOTHING_GRABBED) {
514 reset();
515 invalidate();
516 }
517 if (eventX < mLeftHandleX + hitWindow) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700518 mRotaryOffsetX = eventX - mLeftHandleX;
David Brown88e03752009-10-01 19:25:54 -0700519 setGrabbedState(LEFT_HANDLE_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700520 invalidate();
521 vibrate(VIBRATE_SHORT);
522 } else if (eventX > mRightHandleX - hitWindow) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700523 mRotaryOffsetX = eventX - mRightHandleX;
David Brown88e03752009-10-01 19:25:54 -0700524 setGrabbedState(RIGHT_HANDLE_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700525 invalidate();
526 vibrate(VIBRATE_SHORT);
527 }
528 break;
529
530 case MotionEvent.ACTION_MOVE:
531 if (DBG) log("touch-move");
532 if (mGrabbedState == LEFT_HANDLE_GRABBED) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700533 mRotaryOffsetX = eventX - mLeftHandleX;
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700534 invalidate();
Karl Rosaen74646ad2009-09-23 17:00:55 -0700535 final int rightThresh = isHoriz() ? getRight() : height;
536 if (eventX >= rightThresh - mEdgeTriggerThresh && !mTriggered) {
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700537 mTriggered = true;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700538 dispatchTriggerEvent(OnDialTriggerListener.LEFT_HANDLE);
Karl Rosaen896264f2009-09-22 11:36:23 -0700539 final VelocityTracker velocityTracker = mVelocityTracker;
540 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700541 final int rawVelocity = isHoriz() ?
542 (int) velocityTracker.getXVelocity():
543 -(int) velocityTracker.getYVelocity();
544 final int velocity = Math.max(mMinimumVelocity, rawVelocity);
Karl Rosaen896264f2009-09-22 11:36:23 -0700545 mDimplesOfFling = Math.max(
546 8,
547 Math.abs(velocity / mDimpleSpacing));
548 startAnimationWithVelocity(
549 eventX - mLeftHandleX,
550 mDimplesOfFling * mDimpleSpacing,
551 velocity);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700552 }
553 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700554 mRotaryOffsetX = eventX - mRightHandleX;
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700555 invalidate();
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700556 if (eventX <= mEdgeTriggerThresh && !mTriggered) {
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700557 mTriggered = true;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700558 dispatchTriggerEvent(OnDialTriggerListener.RIGHT_HANDLE);
Karl Rosaen896264f2009-09-22 11:36:23 -0700559 final VelocityTracker velocityTracker = mVelocityTracker;
560 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700561 final int rawVelocity = isHoriz() ?
562 (int) velocityTracker.getXVelocity():
563 - (int) velocityTracker.getYVelocity();
564 final int velocity = Math.min(-mMinimumVelocity, rawVelocity);
Karl Rosaen896264f2009-09-22 11:36:23 -0700565 mDimplesOfFling = Math.max(
566 8,
567 Math.abs(velocity / mDimpleSpacing));
568 startAnimationWithVelocity(
569 eventX - mRightHandleX,
570 -(mDimplesOfFling * mDimpleSpacing),
571 velocity);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700572 }
573 }
574 break;
575 case MotionEvent.ACTION_UP:
576 if (DBG) log("touch-up");
577 // handle animating back to start if they didn't trigger
578 if (mGrabbedState == LEFT_HANDLE_GRABBED
579 && Math.abs(eventX - mLeftHandleX) > 5) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700580 // set up "snap back" animation
Karl Rosaen896264f2009-09-22 11:36:23 -0700581 startAnimation(eventX - mLeftHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700582 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED
583 && Math.abs(eventX - mRightHandleX) > 5) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700584 // set up "snap back" animation
Karl Rosaen896264f2009-09-22 11:36:23 -0700585 startAnimation(eventX - mRightHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700586 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700587 mRotaryOffsetX = 0;
David Brown88e03752009-10-01 19:25:54 -0700588 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700589 invalidate();
Karl Rosaen896264f2009-09-22 11:36:23 -0700590 if (mVelocityTracker != null) {
591 mVelocityTracker.recycle(); // wishin' we had generational GC
592 mVelocityTracker = null;
593 }
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700594 break;
595 case MotionEvent.ACTION_CANCEL:
596 if (DBG) log("touch-cancel");
Karl Rosaene4d95d02009-09-15 17:36:09 -0700597 reset();
598 invalidate();
Karl Rosaen896264f2009-09-22 11:36:23 -0700599 if (mVelocityTracker != null) {
600 mVelocityTracker.recycle();
601 mVelocityTracker = null;
602 }
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700603 break;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700604 }
605 return true;
606 }
607
Karl Rosaen896264f2009-09-22 11:36:23 -0700608 private void startAnimation(int startX, int endX, int duration) {
609 mAnimating = true;
610 mAnimationStartTime = currentAnimationTimeMillis();
611 mAnimationDuration = duration;
612 mAnimatingDeltaXStart = startX;
613 mAnimatingDeltaXEnd = endX;
David Brown88e03752009-10-01 19:25:54 -0700614 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen896264f2009-09-22 11:36:23 -0700615 mDimplesOfFling = 0;
616 invalidate();
617 }
618
619 private void startAnimationWithVelocity(int startX, int endX, int pixelsPerSecond) {
620 mAnimating = true;
621 mAnimationStartTime = currentAnimationTimeMillis();
622 mAnimationDuration = 1000 * (endX - startX) / pixelsPerSecond;
623 mAnimatingDeltaXStart = startX;
624 mAnimatingDeltaXEnd = endX;
David Brown88e03752009-10-01 19:25:54 -0700625 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen896264f2009-09-22 11:36:23 -0700626 invalidate();
627 }
628
629 private void updateAnimation() {
630 final long millisSoFar = currentAnimationTimeMillis() - mAnimationStartTime;
631 final long millisLeft = mAnimationDuration - millisSoFar;
632 final int totalDeltaX = mAnimatingDeltaXStart - mAnimatingDeltaXEnd;
Karl Rosaen74646ad2009-09-23 17:00:55 -0700633 final boolean goingRight = totalDeltaX < 0;
Karl Rosaen896264f2009-09-22 11:36:23 -0700634 if (DBG) log("millisleft for animating: " + millisLeft);
635 if (millisLeft <= 0) {
636 reset();
637 return;
638 }
639 // from 0 to 1 as animation progresses
640 float interpolation =
641 mInterpolator.getInterpolation((float) millisSoFar / mAnimationDuration);
642 final int dx = (int) (totalDeltaX * (1 - interpolation));
643 mRotaryOffsetX = mAnimatingDeltaXEnd + dx;
Karl Rosaen74646ad2009-09-23 17:00:55 -0700644
645 // once we have gone far enough to animate the current buttons off screen, we start
646 // wrapping the offset back to the other side so that when the animation is finished,
647 // the buttons will come back into their original places.
Karl Rosaen896264f2009-09-22 11:36:23 -0700648 if (mDimplesOfFling > 0) {
Karl Rosaenff9c54b2009-09-24 10:50:48 -0700649 if (!goingRight && mRotaryOffsetX < -3 * mDimpleSpacing) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700650 // wrap around on fling left
Karl Rosaen74646ad2009-09-23 17:00:55 -0700651 mRotaryOffsetX += mDimplesOfFling * mDimpleSpacing;
652 } else if (goingRight && mRotaryOffsetX > 3 * mDimpleSpacing) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700653 // wrap around on fling right
Karl Rosaen74646ad2009-09-23 17:00:55 -0700654 mRotaryOffsetX -= mDimplesOfFling * mDimpleSpacing;
Karl Rosaen896264f2009-09-22 11:36:23 -0700655 }
656 }
657 invalidate();
658 }
659
Karl Rosaene4d95d02009-09-15 17:36:09 -0700660 private void reset() {
661 mAnimating = false;
Karl Rosaen896264f2009-09-22 11:36:23 -0700662 mRotaryOffsetX = 0;
663 mDimplesOfFling = 0;
David Brown88e03752009-10-01 19:25:54 -0700664 setGrabbedState(NOTHING_GRABBED);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700665 mTriggered = false;
666 }
667
668 /**
669 * Triggers haptic feedback.
670 */
671 private synchronized void vibrate(long duration) {
Jeff Sharkey723a7252012-10-12 14:26:31 -0700672 final boolean hapticEnabled = Settings.System.getIntForUser(
673 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
674 UserHandle.USER_CURRENT) != 0;
675 if (hapticEnabled) {
676 if (mVibrator == null) {
677 mVibrator = (android.os.Vibrator) getContext()
678 .getSystemService(Context.VIBRATOR_SERVICE);
679 }
680 mVibrator.vibrate(duration);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700681 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700682 }
683
684 /**
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700685 * Draw the bitmap so that it's centered
686 * on the point (x,y), then draws it using specified canvas.
Karl Rosaene4d95d02009-09-15 17:36:09 -0700687 * TODO: is there already a utility method somewhere for this?
688 */
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700689 private void drawCentered(Bitmap d, Canvas c, int x, int y) {
690 int w = d.getWidth();
691 int h = d.getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700692
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700693 c.drawBitmap(d, x - (w / 2), y - (h / 2), mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700694 }
695
696
697 /**
698 * Registers a callback to be invoked when the dial
699 * is "triggered" by rotating it one way or the other.
700 *
701 * @param l the OnDialTriggerListener to attach to this view
702 */
703 public void setOnDialTriggerListener(OnDialTriggerListener l) {
704 mOnDialTriggerListener = l;
705 }
706
707 /**
708 * Dispatches a trigger event to our listener.
709 */
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700710 private void dispatchTriggerEvent(int whichHandle) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700711 vibrate(VIBRATE_LONG);
712 if (mOnDialTriggerListener != null) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700713 mOnDialTriggerListener.onDialTrigger(this, whichHandle);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700714 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700715 }
716
717 /**
David Brown88e03752009-10-01 19:25:54 -0700718 * Sets the current grabbed state, and dispatches a grabbed state change
719 * event to our listener.
720 */
721 private void setGrabbedState(int newState) {
722 if (newState != mGrabbedState) {
723 mGrabbedState = newState;
724 if (mOnDialTriggerListener != null) {
725 mOnDialTriggerListener.onGrabbedStateChange(this, mGrabbedState);
726 }
727 }
728 }
729
730 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700731 * Interface definition for a callback to be invoked when the dial
732 * is "triggered" by rotating it one way or the other.
733 */
734 public interface OnDialTriggerListener {
735 /**
736 * The dial was triggered because the user grabbed the left handle,
737 * and rotated the dial clockwise.
738 */
739 public static final int LEFT_HANDLE = 1;
740
741 /**
742 * The dial was triggered because the user grabbed the right handle,
743 * and rotated the dial counterclockwise.
744 */
745 public static final int RIGHT_HANDLE = 2;
746
747 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700748 * Called when the dial is triggered.
749 *
750 * @param v The view that was triggered
751 * @param whichHandle Which "dial handle" the user grabbed,
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700752 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
Karl Rosaene4d95d02009-09-15 17:36:09 -0700753 */
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700754 void onDialTrigger(View v, int whichHandle);
David Brown88e03752009-10-01 19:25:54 -0700755
756 /**
757 * Called when the "grabbed state" changes (i.e. when
758 * the user either grabs or releases one of the handles.)
759 *
760 * @param v the view that was triggered
761 * @param grabbedState the new state: either {@link #NOTHING_GRABBED},
762 * {@link #LEFT_HANDLE_GRABBED}, or {@link #RIGHT_HANDLE_GRABBED}.
763 */
764 void onGrabbedStateChange(View v, int grabbedState);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700765 }
766
767
768 // Debugging / testing code
769
770 private void log(String msg) {
771 Log.d(LOG_TAG, msg);
772 }
773}