blob: f856027e27087c6c24e99848c01fb463e856afd3 [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;
Jeff Sharkey723a7252012-10-12 14:26:31 -070027import android.os.UserHandle;
Karl Rosaene4d95d02009-09-15 17:36:09 -070028import android.os.Vibrator;
Jeff Sharkey723a7252012-10-12 14:26:31 -070029import android.provider.Settings;
Karl Rosaene4d95d02009-09-15 17:36:09 -070030import android.util.AttributeSet;
31import android.util.Log;
32import android.view.MotionEvent;
33import android.view.View;
Karl Rosaen896264f2009-09-22 11:36:23 -070034import android.view.VelocityTracker;
35import android.view.ViewConfiguration;
36import android.view.animation.DecelerateInterpolator;
Karl Rosaen1ca654e2009-09-16 10:51:10 -070037import static android.view.animation.AnimationUtils.currentAnimationTimeMillis;
Karl Rosaene4d95d02009-09-15 17:36:09 -070038import com.android.internal.R;
39
40
41/**
42 * Custom view that presents up to two items that are selectable by rotating a semi-circle from
43 * left to right, or right to left. Used by incoming call screen, and the lock screen when no
44 * security pattern is set.
45 */
46public class RotarySelector extends View {
Karl Rosaen74646ad2009-09-23 17:00:55 -070047 public static final int HORIZONTAL = 0;
48 public static final int VERTICAL = 1;
49
Karl Rosaene4d95d02009-09-15 17:36:09 -070050 private static final String LOG_TAG = "RotarySelector";
51 private static final boolean DBG = false;
Jim Miller5037e572009-10-05 13:00:58 -070052 private static final boolean VISUAL_DEBUG = false;
Karl Rosaene4d95d02009-09-15 17:36:09 -070053
54 // Listener for onDialTrigger() callbacks.
55 private OnDialTriggerListener mOnDialTriggerListener;
56
57 private float mDensity;
58
59 // UI elements
Karl Rosaen74646ad2009-09-23 17:00:55 -070060 private Bitmap mBackground;
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -070061 private Bitmap mDimple;
Jim Millerd9b6f142009-09-30 22:50:01 -070062 private Bitmap mDimpleDim;
Karl Rosaene4d95d02009-09-15 17:36:09 -070063
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -070064 private Bitmap mLeftHandleIcon;
65 private Bitmap mRightHandleIcon;
Karl Rosaene4d95d02009-09-15 17:36:09 -070066
Karl Rosaen74646ad2009-09-23 17:00:55 -070067 private Bitmap mArrowShortLeftAndRight;
68 private Bitmap mArrowLongLeft; // Long arrow starting on the left, pointing clockwise
69 private Bitmap mArrowLongRight; // Long arrow starting on the right, pointing CCW
Karl Rosaene4d95d02009-09-15 17:36:09 -070070
71 // positions of the left and right handle
72 private int mLeftHandleX;
73 private int mRightHandleX;
74
Karl Rosaen896264f2009-09-22 11:36:23 -070075 // current offset of rotary widget along the x axis
76 private int mRotaryOffsetX = 0;
Karl Rosaene4d95d02009-09-15 17:36:09 -070077
78 // state of the animation used to bring the handle back to its start position when
79 // the user lets go before triggering an action
80 private boolean mAnimating = false;
Karl Rosaen896264f2009-09-22 11:36:23 -070081 private long mAnimationStartTime;
Karl Rosaen052e1872009-09-20 15:03:20 -070082 private long mAnimationDuration;
Karl Rosaen896264f2009-09-22 11:36:23 -070083 private int mAnimatingDeltaXStart; // the animation will interpolate from this delta to zero
84 private int mAnimatingDeltaXEnd;
85
86 private DecelerateInterpolator mInterpolator;
Karl Rosaene4d95d02009-09-15 17:36:09 -070087
Karl Rosaen74646ad2009-09-23 17:00:55 -070088 private Paint mPaint = new Paint();
89
90 // used to rotate the background and arrow assets depending on orientation
91 final Matrix mBgMatrix = new Matrix();
92 final Matrix mArrowMatrix = new Matrix();
93
Karl Rosaene4d95d02009-09-15 17:36:09 -070094 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -070095 * If the user is currently dragging something.
96 */
97 private int mGrabbedState = NOTHING_GRABBED;
David Brown88e03752009-10-01 19:25:54 -070098 public static final int NOTHING_GRABBED = 0;
99 public static final int LEFT_HANDLE_GRABBED = 1;
100 public static final int RIGHT_HANDLE_GRABBED = 2;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700101
102 /**
103 * Whether the user has triggered something (e.g dragging the left handle all the way over to
104 * the right).
105 */
106 private boolean mTriggered = false;
107
108 // Vibration (haptic feedback)
109 private Vibrator mVibrator;
Jim Miller9485aec2009-10-08 18:49:53 -0700110 private static final long VIBRATE_SHORT = 20; // msec
111 private static final long VIBRATE_LONG = 20; // msec
Karl Rosaene4d95d02009-09-15 17:36:09 -0700112
Karl Rosaene4d95d02009-09-15 17:36:09 -0700113 /**
114 * The drawable for the arrows need to be scrunched this many dips towards the rotary bg below
115 * it.
116 */
117 private static final int ARROW_SCRUNCH_DIP = 6;
118
119 /**
120 * How far inset the left and right circles should be
121 */
122 private static final int EDGE_PADDING_DIP = 9;
123
124 /**
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700125 * How far from the edge of the screen the user must drag to trigger the event.
126 */
Karl Rosaen052e1872009-09-20 15:03:20 -0700127 private static final int EDGE_TRIGGER_DIP = 100;
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700128
129 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700130 * Dimensions of arc in background drawable.
131 */
132 static final int OUTER_ROTARY_RADIUS_DIP = 390;
133 static final int ROTARY_STROKE_WIDTH_DIP = 83;
Karl Rosaen052e1872009-09-20 15:03:20 -0700134 static final int SNAP_BACK_ANIMATION_DURATION_MILLIS = 300;
135 static final int SPIN_ANIMATION_DURATION_MILLIS = 800;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700136
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700137 private int mEdgeTriggerThresh;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700138 private int mDimpleWidth;
139 private int mBackgroundWidth;
140 private int mBackgroundHeight;
Karl Rosaen896264f2009-09-22 11:36:23 -0700141 private final int mOuterRadius;
142 private final int mInnerRadius;
143 private int mDimpleSpacing;
144
145 private VelocityTracker mVelocityTracker;
146 private int mMinimumVelocity;
147 private int mMaximumVelocity;
148
149 /**
150 * The number of dimples we are flinging when we do the "spin" animation. Used to know when to
151 * wrap the icons back around so they "rotate back" onto the screen.
152 * @see #updateAnimation()
153 */
154 private int mDimplesOfFling = 0;
155
Karl Rosaen74646ad2009-09-23 17:00:55 -0700156 /**
157 * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
158 */
159 private int mOrientation;
Karl Rosaen896264f2009-09-22 11:36:23 -0700160
Karl Rosaene4d95d02009-09-15 17:36:09 -0700161
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700162 public RotarySelector(Context context) {
163 this(context, null);
164 }
165
Karl Rosaene4d95d02009-09-15 17:36:09 -0700166 /**
167 * Constructor used when this widget is created from a layout file.
168 */
169 public RotarySelector(Context context, AttributeSet attrs) {
170 super(context, attrs);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700171
172 TypedArray a =
173 context.obtainStyledAttributes(attrs, R.styleable.RotarySelector);
174 mOrientation = a.getInt(R.styleable.RotarySelector_orientation, HORIZONTAL);
175 a.recycle();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700176
177 Resources r = getResources();
178 mDensity = r.getDisplayMetrics().density;
179 if (DBG) log("- Density: " + mDensity);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700180
181 // Assets (all are BitmapDrawables).
Karl Rosaen74646ad2009-09-23 17:00:55 -0700182 mBackground = getBitmapFor(R.drawable.jog_dial_bg);
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700183 mDimple = getBitmapFor(R.drawable.jog_dial_dimple);
Jim Millerd9b6f142009-09-30 22:50:01 -0700184 mDimpleDim = getBitmapFor(R.drawable.jog_dial_dimple_dim);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700185
Karl Rosaen74646ad2009-09-23 17:00:55 -0700186 mArrowLongLeft = getBitmapFor(R.drawable.jog_dial_arrow_long_left_green);
187 mArrowLongRight = getBitmapFor(R.drawable.jog_dial_arrow_long_right_red);
188 mArrowShortLeftAndRight = getBitmapFor(R.drawable.jog_dial_arrow_short_left_and_right);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700189
Karl Rosaen896264f2009-09-22 11:36:23 -0700190 mInterpolator = new DecelerateInterpolator(1f);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700191
192 mEdgeTriggerThresh = (int) (mDensity * EDGE_TRIGGER_DIP);
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700193
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700194 mDimpleWidth = mDimple.getWidth();
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700195
Karl Rosaen74646ad2009-09-23 17:00:55 -0700196 mBackgroundWidth = mBackground.getWidth();
197 mBackgroundHeight = mBackground.getHeight();
Karl Rosaen896264f2009-09-22 11:36:23 -0700198 mOuterRadius = (int) (mDensity * OUTER_ROTARY_RADIUS_DIP);
199 mInnerRadius = (int) ((OUTER_ROTARY_RADIUS_DIP - ROTARY_STROKE_WIDTH_DIP) * mDensity);
200
201 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
202 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity() * 2;
203 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
204 }
205
Karl Rosaen74646ad2009-09-23 17:00:55 -0700206 private Bitmap getBitmapFor(int resId) {
207 return BitmapFactory.decodeResource(getContext().getResources(), resId);
208 }
209
Karl Rosaen896264f2009-09-22 11:36:23 -0700210 @Override
211 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
212 super.onSizeChanged(w, h, oldw, oldh);
213
Karl Rosaen74646ad2009-09-23 17:00:55 -0700214 final int edgePadding = (int) (EDGE_PADDING_DIP * mDensity);
215 mLeftHandleX = edgePadding + mDimpleWidth / 2;
216 final int length = isHoriz() ? w : h;
217 mRightHandleX = length - edgePadding - mDimpleWidth / 2;
218 mDimpleSpacing = (length / 2) - mLeftHandleX;
Karl Rosaen896264f2009-09-22 11:36:23 -0700219
Karl Rosaen74646ad2009-09-23 17:00:55 -0700220 // bg matrix only needs to be calculated once
221 mBgMatrix.setTranslate(0, 0);
222 if (!isHoriz()) {
223 // set up matrix for translating drawing of background and arrow assets
224 final int left = w - mBackgroundHeight;
225 mBgMatrix.preRotate(-90, 0, 0);
226 mBgMatrix.postTranslate(left, h);
227
228 } else {
229 mBgMatrix.postTranslate(0, h - mBackgroundHeight);
230 }
231 }
232
233 private boolean isHoriz() {
234 return mOrientation == HORIZONTAL;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700235 }
236
237 /**
238 * Sets the left handle icon to a given resource.
239 *
240 * The resource should refer to a Drawable object, or use 0 to remove
241 * the icon.
242 *
243 * @param resId the resource ID.
244 */
245 public void setLeftHandleResource(int resId) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700246 if (resId != 0) {
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700247 mLeftHandleIcon = getBitmapFor(resId);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700248 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700249 invalidate();
250 }
251
252 /**
253 * Sets the right handle icon to a given resource.
254 *
255 * The resource should refer to a Drawable object, or use 0 to remove
256 * the icon.
257 *
258 * @param resId the resource ID.
259 */
260 public void setRightHandleResource(int resId) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700261 if (resId != 0) {
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700262 mRightHandleIcon = getBitmapFor(resId);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700263 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700264 invalidate();
265 }
266
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700267
Karl Rosaene4d95d02009-09-15 17:36:09 -0700268 @Override
269 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Karl Rosaen74646ad2009-09-23 17:00:55 -0700270 final int length = isHoriz() ?
271 MeasureSpec.getSize(widthMeasureSpec) :
272 MeasureSpec.getSize(heightMeasureSpec);
273 final int arrowScrunch = (int) (ARROW_SCRUNCH_DIP * mDensity);
274 final int arrowH = mArrowShortLeftAndRight.getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700275
276 // by making the height less than arrow + bg, arrow and bg will be scrunched together,
277 // overlaying somewhat (though on transparent portions of the drawable).
278 // this works because the arrows are drawn from the top, and the rotary bg is drawn
279 // from the bottom.
Karl Rosaen74646ad2009-09-23 17:00:55 -0700280 final int height = mBackgroundHeight + arrowH - arrowScrunch;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700281
Karl Rosaen74646ad2009-09-23 17:00:55 -0700282 if (isHoriz()) {
283 setMeasuredDimension(length, height);
284 } else {
285 setMeasuredDimension(height, length);
286 }
287 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700288
289 @Override
290 protected void onDraw(Canvas canvas) {
291 super.onDraw(canvas);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700292
293 final int width = getWidth();
294
Jim Miller5037e572009-10-05 13:00:58 -0700295 if (VISUAL_DEBUG) {
296 // draw bounding box around widget
297 mPaint.setColor(0xffff0000);
298 mPaint.setStyle(Paint.Style.STROKE);
299 canvas.drawRect(0, 0, width, getHeight(), mPaint);
300 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700301
302 final int height = getHeight();
303
304 // update animating state before we draw anything
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700305 if (mAnimating) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700306 updateAnimation();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700307 }
308
Karl Rosaene4d95d02009-09-15 17:36:09 -0700309 // Background:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700310 canvas.drawBitmap(mBackground, mBgMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700311
312 // Draw the correct arrow(s) depending on the current state:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700313 mArrowMatrix.reset();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700314 switch (mGrabbedState) {
315 case NOTHING_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700316 //mArrowShortLeftAndRight;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700317 break;
318 case LEFT_HANDLE_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700319 mArrowMatrix.setTranslate(0, 0);
320 if (!isHoriz()) {
321 mArrowMatrix.preRotate(-90, 0, 0);
322 mArrowMatrix.postTranslate(0, height);
323 }
324 canvas.drawBitmap(mArrowLongLeft, mArrowMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700325 break;
326 case RIGHT_HANDLE_GRABBED:
Karl Rosaen74646ad2009-09-23 17:00:55 -0700327 mArrowMatrix.setTranslate(0, 0);
328 if (!isHoriz()) {
329 mArrowMatrix.preRotate(-90, 0, 0);
330 // since bg width is > height of screen in landscape mode...
331 mArrowMatrix.postTranslate(0, height + (mBackgroundWidth - height));
332 }
333 canvas.drawBitmap(mArrowLongRight, mArrowMatrix, mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700334 break;
335 default:
336 throw new IllegalStateException("invalid mGrabbedState: " + mGrabbedState);
337 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700338
Karl Rosaen74646ad2009-09-23 17:00:55 -0700339 final int bgHeight = mBackgroundHeight;
340 final int bgTop = isHoriz() ?
341 height - bgHeight:
342 width - bgHeight;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700343
Jim Miller5037e572009-10-05 13:00:58 -0700344 if (VISUAL_DEBUG) {
345 // draw circle bounding arc drawable: good sanity check we're doing the math correctly
346 float or = OUTER_ROTARY_RADIUS_DIP * mDensity;
347 final int vOffset = mBackgroundWidth - height;
348 final int midX = isHoriz() ? width / 2 : mBackgroundWidth / 2 - vOffset;
349 if (isHoriz()) {
350 canvas.drawCircle(midX, or + bgTop, or, mPaint);
351 } else {
352 canvas.drawCircle(or + bgTop, midX, or, mPaint);
353 }
354 }
Jim Millerd9b6f142009-09-30 22:50:01 -0700355
Karl Rosaen74646ad2009-09-23 17:00:55 -0700356 // left dimple / icon
Karl Rosaene4d95d02009-09-15 17:36:09 -0700357 {
Karl Rosaen896264f2009-09-22 11:36:23 -0700358 final int xOffset = mLeftHandleX + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700359 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700360 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700361 mInnerRadius,
362 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700363 xOffset);
Jim Miller5037e572009-10-05 13:00:58 -0700364 final int x = isHoriz() ? xOffset : drawableY + bgTop;
365 final int y = isHoriz() ? drawableY + bgTop : height - xOffset;
366 if (mGrabbedState != RIGHT_HANDLE_GRABBED) {
367 drawCentered(mDimple, canvas, x, y);
368 drawCentered(mLeftHandleIcon, canvas, x, y);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700369 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700370 drawCentered(mDimpleDim, canvas, x, y);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700371 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700372 }
373
Karl Rosaen74646ad2009-09-23 17:00:55 -0700374 // center dimple
375 {
376 final int xOffset = isHoriz() ?
377 width / 2 + mRotaryOffsetX:
378 height / 2 + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700379 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700380 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700381 mInnerRadius,
382 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700383 xOffset);
384
Karl Rosaen74646ad2009-09-23 17:00:55 -0700385 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700386 drawCentered(mDimpleDim, canvas, xOffset, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700387 } else {
388 // vertical
Jim Miller5037e572009-10-05 13:00:58 -0700389 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - xOffset);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700390 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700391 }
392
Karl Rosaen74646ad2009-09-23 17:00:55 -0700393 // right dimple / icon
Karl Rosaene4d95d02009-09-15 17:36:09 -0700394 {
Karl Rosaen896264f2009-09-22 11:36:23 -0700395 final int xOffset = mRightHandleX + mRotaryOffsetX;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700396 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700397 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700398 mInnerRadius,
399 mOuterRadius,
Karl Rosaene4d95d02009-09-15 17:36:09 -0700400 xOffset);
401
Jim Miller5037e572009-10-05 13:00:58 -0700402 final int x = isHoriz() ? xOffset : drawableY + bgTop;
403 final int y = isHoriz() ? drawableY + bgTop : height - xOffset;
404 if (mGrabbedState != LEFT_HANDLE_GRABBED) {
405 drawCentered(mDimple, canvas, x, y);
406 drawCentered(mRightHandleIcon, canvas, x, y);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700407 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700408 drawCentered(mDimpleDim, canvas, x, y);
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700409 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700410 }
411
Karl Rosaen896264f2009-09-22 11:36:23 -0700412 // draw extra left hand dimples
413 int dimpleLeft = mRotaryOffsetX + mLeftHandleX - mDimpleSpacing;
414 final int halfdimple = mDimpleWidth / 2;
415 while (dimpleLeft > -halfdimple) {
416 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700417 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700418 mInnerRadius,
419 mOuterRadius,
420 dimpleLeft);
421
Karl Rosaen74646ad2009-09-23 17:00:55 -0700422 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700423 drawCentered(mDimpleDim, canvas, dimpleLeft, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700424 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700425 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleLeft);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700426 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700427 dimpleLeft -= mDimpleSpacing;
428 }
429
430 // draw extra right hand dimples
431 int dimpleRight = mRotaryOffsetX + mRightHandleX + mDimpleSpacing;
432 final int rightThresh = mRight + halfdimple;
433 while (dimpleRight < rightThresh) {
434 final int drawableY = getYOnArc(
Karl Rosaen74646ad2009-09-23 17:00:55 -0700435 mBackgroundWidth,
Karl Rosaen896264f2009-09-22 11:36:23 -0700436 mInnerRadius,
437 mOuterRadius,
438 dimpleRight);
439
Karl Rosaen74646ad2009-09-23 17:00:55 -0700440 if (isHoriz()) {
Jim Miller5037e572009-10-05 13:00:58 -0700441 drawCentered(mDimpleDim, canvas, dimpleRight, drawableY + bgTop);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700442 } else {
Jim Miller5037e572009-10-05 13:00:58 -0700443 drawCentered(mDimpleDim, canvas, drawableY + bgTop, height - dimpleRight);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700444 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700445 dimpleRight += mDimpleSpacing;
446 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700447 }
448
449 /**
Karl Rosaen74646ad2009-09-23 17:00:55 -0700450 * Assuming bitmap is a bounding box around a piece of an arc drawn by two concentric circles
Karl Rosaene4d95d02009-09-15 17:36:09 -0700451 * (as the background drawable for the rotary widget is), and given an x coordinate along the
452 * drawable, return the y coordinate of a point on the arc that is between the two concentric
453 * circles. The resulting y combined with the incoming x is a point along the circle in
454 * between the two concentric circles.
455 *
Karl Rosaen74646ad2009-09-23 17:00:55 -0700456 * @param backgroundWidth The width of the asset (the bottom of the box surrounding the arc).
Karl Rosaene4d95d02009-09-15 17:36:09 -0700457 * @param innerRadius The radius of the circle that intersects the drawable at the bottom two
458 * corders of the drawable (top two corners in terms of drawing coordinates).
459 * @param outerRadius The radius of the circle who's top most point is the top center of the
460 * drawable (bottom center in terms of drawing coordinates).
Karl Rosaen74646ad2009-09-23 17:00:55 -0700461 * @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 -0700462 * in between the two concentric circles.
463 */
Karl Rosaen74646ad2009-09-23 17:00:55 -0700464 private int getYOnArc(int backgroundWidth, int innerRadius, int outerRadius, int x) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700465
466 // the hypotenuse
467 final int halfWidth = (outerRadius - innerRadius) / 2;
468 final int middleRadius = innerRadius + halfWidth;
469
470 // the bottom leg of the triangle
Karl Rosaen74646ad2009-09-23 17:00:55 -0700471 final int triangleBottom = (backgroundWidth / 2) - x;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700472
473 // "Our offense is like the pythagorean theorem: There is no answer!" - Shaquille O'Neal
474 final int triangleY =
475 (int) Math.sqrt(middleRadius * middleRadius - triangleBottom * triangleBottom);
476
477 // convert to drawing coordinates:
478 // middleRadius - triangleY =
479 // the vertical distance from the outer edge of the circle to the desired point
480 // from there we add the distance from the top of the drawable to the middle circle
481 return middleRadius - triangleY + halfWidth;
482 }
483
484 /**
485 * Handle touch screen events.
486 *
487 * @param event The motion event.
488 * @return True if the event was handled, false otherwise.
489 */
490 @Override
491 public boolean onTouchEvent(MotionEvent event) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700492 if (mAnimating) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700493 return true;
494 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700495 if (mVelocityTracker == null) {
496 mVelocityTracker = VelocityTracker.obtain();
497 }
498 mVelocityTracker.addMovement(event);
499
Karl Rosaen74646ad2009-09-23 17:00:55 -0700500 final int height = getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700501
Karl Rosaen74646ad2009-09-23 17:00:55 -0700502 final int eventX = isHoriz() ?
503 (int) event.getX():
504 height - ((int) event.getY());
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700505 final int hitWindow = mDimpleWidth;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700506
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700507 final int action = event.getAction();
508 switch (action) {
509 case MotionEvent.ACTION_DOWN:
510 if (DBG) log("touch-down");
511 mTriggered = false;
512 if (mGrabbedState != NOTHING_GRABBED) {
513 reset();
514 invalidate();
515 }
516 if (eventX < mLeftHandleX + hitWindow) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700517 mRotaryOffsetX = eventX - mLeftHandleX;
David Brown88e03752009-10-01 19:25:54 -0700518 setGrabbedState(LEFT_HANDLE_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700519 invalidate();
520 vibrate(VIBRATE_SHORT);
521 } else if (eventX > mRightHandleX - hitWindow) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700522 mRotaryOffsetX = eventX - mRightHandleX;
David Brown88e03752009-10-01 19:25:54 -0700523 setGrabbedState(RIGHT_HANDLE_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700524 invalidate();
525 vibrate(VIBRATE_SHORT);
526 }
527 break;
528
529 case MotionEvent.ACTION_MOVE:
530 if (DBG) log("touch-move");
531 if (mGrabbedState == LEFT_HANDLE_GRABBED) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700532 mRotaryOffsetX = eventX - mLeftHandleX;
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700533 invalidate();
Karl Rosaen74646ad2009-09-23 17:00:55 -0700534 final int rightThresh = isHoriz() ? getRight() : height;
535 if (eventX >= rightThresh - mEdgeTriggerThresh && !mTriggered) {
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700536 mTriggered = true;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700537 dispatchTriggerEvent(OnDialTriggerListener.LEFT_HANDLE);
Karl Rosaen896264f2009-09-22 11:36:23 -0700538 final VelocityTracker velocityTracker = mVelocityTracker;
539 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700540 final int rawVelocity = isHoriz() ?
541 (int) velocityTracker.getXVelocity():
542 -(int) velocityTracker.getYVelocity();
543 final int velocity = Math.max(mMinimumVelocity, rawVelocity);
Karl Rosaen896264f2009-09-22 11:36:23 -0700544 mDimplesOfFling = Math.max(
545 8,
546 Math.abs(velocity / mDimpleSpacing));
547 startAnimationWithVelocity(
548 eventX - mLeftHandleX,
549 mDimplesOfFling * mDimpleSpacing,
550 velocity);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700551 }
552 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700553 mRotaryOffsetX = eventX - mRightHandleX;
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700554 invalidate();
Karl Rosaen5fef93b2009-09-17 17:21:18 -0700555 if (eventX <= mEdgeTriggerThresh && !mTriggered) {
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700556 mTriggered = true;
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700557 dispatchTriggerEvent(OnDialTriggerListener.RIGHT_HANDLE);
Karl Rosaen896264f2009-09-22 11:36:23 -0700558 final VelocityTracker velocityTracker = mVelocityTracker;
559 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Karl Rosaen74646ad2009-09-23 17:00:55 -0700560 final int rawVelocity = isHoriz() ?
561 (int) velocityTracker.getXVelocity():
562 - (int) velocityTracker.getYVelocity();
563 final int velocity = Math.min(-mMinimumVelocity, rawVelocity);
Karl Rosaen896264f2009-09-22 11:36:23 -0700564 mDimplesOfFling = Math.max(
565 8,
566 Math.abs(velocity / mDimpleSpacing));
567 startAnimationWithVelocity(
568 eventX - mRightHandleX,
569 -(mDimplesOfFling * mDimpleSpacing),
570 velocity);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700571 }
572 }
573 break;
574 case MotionEvent.ACTION_UP:
575 if (DBG) log("touch-up");
576 // handle animating back to start if they didn't trigger
577 if (mGrabbedState == LEFT_HANDLE_GRABBED
578 && Math.abs(eventX - mLeftHandleX) > 5) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700579 // set up "snap back" animation
Karl Rosaen896264f2009-09-22 11:36:23 -0700580 startAnimation(eventX - mLeftHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700581 } else if (mGrabbedState == RIGHT_HANDLE_GRABBED
582 && Math.abs(eventX - mRightHandleX) > 5) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700583 // set up "snap back" animation
Karl Rosaen896264f2009-09-22 11:36:23 -0700584 startAnimation(eventX - mRightHandleX, 0, SNAP_BACK_ANIMATION_DURATION_MILLIS);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700585 }
Karl Rosaen896264f2009-09-22 11:36:23 -0700586 mRotaryOffsetX = 0;
David Brown88e03752009-10-01 19:25:54 -0700587 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700588 invalidate();
Karl Rosaen896264f2009-09-22 11:36:23 -0700589 if (mVelocityTracker != null) {
590 mVelocityTracker.recycle(); // wishin' we had generational GC
591 mVelocityTracker = null;
592 }
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700593 break;
594 case MotionEvent.ACTION_CANCEL:
595 if (DBG) log("touch-cancel");
Karl Rosaene4d95d02009-09-15 17:36:09 -0700596 reset();
597 invalidate();
Karl Rosaen896264f2009-09-22 11:36:23 -0700598 if (mVelocityTracker != null) {
599 mVelocityTracker.recycle();
600 mVelocityTracker = null;
601 }
Karl Rosaen1ca654e2009-09-16 10:51:10 -0700602 break;
Karl Rosaene4d95d02009-09-15 17:36:09 -0700603 }
604 return true;
605 }
606
Karl Rosaen896264f2009-09-22 11:36:23 -0700607 private void startAnimation(int startX, int endX, int duration) {
608 mAnimating = true;
609 mAnimationStartTime = currentAnimationTimeMillis();
610 mAnimationDuration = duration;
611 mAnimatingDeltaXStart = startX;
612 mAnimatingDeltaXEnd = endX;
David Brown88e03752009-10-01 19:25:54 -0700613 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen896264f2009-09-22 11:36:23 -0700614 mDimplesOfFling = 0;
615 invalidate();
616 }
617
618 private void startAnimationWithVelocity(int startX, int endX, int pixelsPerSecond) {
619 mAnimating = true;
620 mAnimationStartTime = currentAnimationTimeMillis();
621 mAnimationDuration = 1000 * (endX - startX) / pixelsPerSecond;
622 mAnimatingDeltaXStart = startX;
623 mAnimatingDeltaXEnd = endX;
David Brown88e03752009-10-01 19:25:54 -0700624 setGrabbedState(NOTHING_GRABBED);
Karl Rosaen896264f2009-09-22 11:36:23 -0700625 invalidate();
626 }
627
628 private void updateAnimation() {
629 final long millisSoFar = currentAnimationTimeMillis() - mAnimationStartTime;
630 final long millisLeft = mAnimationDuration - millisSoFar;
631 final int totalDeltaX = mAnimatingDeltaXStart - mAnimatingDeltaXEnd;
Karl Rosaen74646ad2009-09-23 17:00:55 -0700632 final boolean goingRight = totalDeltaX < 0;
Karl Rosaen896264f2009-09-22 11:36:23 -0700633 if (DBG) log("millisleft for animating: " + millisLeft);
634 if (millisLeft <= 0) {
635 reset();
636 return;
637 }
638 // from 0 to 1 as animation progresses
639 float interpolation =
640 mInterpolator.getInterpolation((float) millisSoFar / mAnimationDuration);
641 final int dx = (int) (totalDeltaX * (1 - interpolation));
642 mRotaryOffsetX = mAnimatingDeltaXEnd + dx;
Karl Rosaen74646ad2009-09-23 17:00:55 -0700643
644 // once we have gone far enough to animate the current buttons off screen, we start
645 // wrapping the offset back to the other side so that when the animation is finished,
646 // the buttons will come back into their original places.
Karl Rosaen896264f2009-09-22 11:36:23 -0700647 if (mDimplesOfFling > 0) {
Karl Rosaenff9c54b2009-09-24 10:50:48 -0700648 if (!goingRight && mRotaryOffsetX < -3 * mDimpleSpacing) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700649 // wrap around on fling left
Karl Rosaen74646ad2009-09-23 17:00:55 -0700650 mRotaryOffsetX += mDimplesOfFling * mDimpleSpacing;
651 } else if (goingRight && mRotaryOffsetX > 3 * mDimpleSpacing) {
Karl Rosaen896264f2009-09-22 11:36:23 -0700652 // wrap around on fling right
Karl Rosaen74646ad2009-09-23 17:00:55 -0700653 mRotaryOffsetX -= mDimplesOfFling * mDimpleSpacing;
Karl Rosaen896264f2009-09-22 11:36:23 -0700654 }
655 }
656 invalidate();
657 }
658
Karl Rosaene4d95d02009-09-15 17:36:09 -0700659 private void reset() {
660 mAnimating = false;
Karl Rosaen896264f2009-09-22 11:36:23 -0700661 mRotaryOffsetX = 0;
662 mDimplesOfFling = 0;
David Brown88e03752009-10-01 19:25:54 -0700663 setGrabbedState(NOTHING_GRABBED);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700664 mTriggered = false;
665 }
666
667 /**
668 * Triggers haptic feedback.
669 */
670 private synchronized void vibrate(long duration) {
Jeff Sharkey723a7252012-10-12 14:26:31 -0700671 final boolean hapticEnabled = Settings.System.getIntForUser(
672 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
673 UserHandle.USER_CURRENT) != 0;
674 if (hapticEnabled) {
675 if (mVibrator == null) {
676 mVibrator = (android.os.Vibrator) getContext()
677 .getSystemService(Context.VIBRATOR_SERVICE);
678 }
679 mVibrator.vibrate(duration);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700680 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700681 }
682
683 /**
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700684 * Draw the bitmap so that it's centered
685 * on the point (x,y), then draws it using specified canvas.
Karl Rosaene4d95d02009-09-15 17:36:09 -0700686 * TODO: is there already a utility method somewhere for this?
687 */
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700688 private void drawCentered(Bitmap d, Canvas c, int x, int y) {
689 int w = d.getWidth();
690 int h = d.getHeight();
Karl Rosaene4d95d02009-09-15 17:36:09 -0700691
Karl Rosaenc8ad6dc2009-09-25 11:34:50 -0700692 c.drawBitmap(d, x - (w / 2), y - (h / 2), mPaint);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700693 }
694
695
696 /**
697 * Registers a callback to be invoked when the dial
698 * is "triggered" by rotating it one way or the other.
699 *
700 * @param l the OnDialTriggerListener to attach to this view
701 */
702 public void setOnDialTriggerListener(OnDialTriggerListener l) {
703 mOnDialTriggerListener = l;
704 }
705
706 /**
707 * Dispatches a trigger event to our listener.
708 */
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700709 private void dispatchTriggerEvent(int whichHandle) {
Karl Rosaene4d95d02009-09-15 17:36:09 -0700710 vibrate(VIBRATE_LONG);
711 if (mOnDialTriggerListener != null) {
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700712 mOnDialTriggerListener.onDialTrigger(this, whichHandle);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700713 }
Karl Rosaene4d95d02009-09-15 17:36:09 -0700714 }
715
716 /**
David Brown88e03752009-10-01 19:25:54 -0700717 * Sets the current grabbed state, and dispatches a grabbed state change
718 * event to our listener.
719 */
720 private void setGrabbedState(int newState) {
721 if (newState != mGrabbedState) {
722 mGrabbedState = newState;
723 if (mOnDialTriggerListener != null) {
724 mOnDialTriggerListener.onGrabbedStateChange(this, mGrabbedState);
725 }
726 }
727 }
728
729 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700730 * Interface definition for a callback to be invoked when the dial
731 * is "triggered" by rotating it one way or the other.
732 */
733 public interface OnDialTriggerListener {
734 /**
735 * The dial was triggered because the user grabbed the left handle,
736 * and rotated the dial clockwise.
737 */
738 public static final int LEFT_HANDLE = 1;
739
740 /**
741 * The dial was triggered because the user grabbed the right handle,
742 * and rotated the dial counterclockwise.
743 */
744 public static final int RIGHT_HANDLE = 2;
745
746 /**
Karl Rosaene4d95d02009-09-15 17:36:09 -0700747 * Called when the dial is triggered.
748 *
749 * @param v The view that was triggered
750 * @param whichHandle Which "dial handle" the user grabbed,
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700751 * either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
Karl Rosaene4d95d02009-09-15 17:36:09 -0700752 */
Karl Rosaen278ec5d62009-09-20 09:48:53 -0700753 void onDialTrigger(View v, int whichHandle);
David Brown88e03752009-10-01 19:25:54 -0700754
755 /**
756 * Called when the "grabbed state" changes (i.e. when
757 * the user either grabs or releases one of the handles.)
758 *
759 * @param v the view that was triggered
760 * @param grabbedState the new state: either {@link #NOTHING_GRABBED},
761 * {@link #LEFT_HANDLE_GRABBED}, or {@link #RIGHT_HANDLE_GRABBED}.
762 */
763 void onGrabbedStateChange(View v, int grabbedState);
Karl Rosaene4d95d02009-09-15 17:36:09 -0700764 }
765
766
767 // Debugging / testing code
768
769 private void log(String msg) {
770 Log.d(LOG_TAG, msg);
771 }
772}