blob: 93ea5b304a985ecf09291d5d4b8d1a604afc8afc [file] [log] [blame]
Jim Miller955a0162012-06-11 21:06:13 -07001/*
2 * Copyright (C) 2012 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.multiwaveview;
18
19import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.TimeInterpolator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.pm.PackageManager;
28import android.content.pm.PackageManager.NameNotFoundException;
29import android.content.res.Resources;
30import android.content.res.TypedArray;
31import android.graphics.Canvas;
Jim Miller955a0162012-06-11 21:06:13 -070032import android.graphics.drawable.Drawable;
33import android.os.Bundle;
Jeff Sharkey723a7252012-10-12 14:26:31 -070034import android.os.UserHandle;
Jim Miller955a0162012-06-11 21:06:13 -070035import android.os.Vibrator;
Jeff Sharkey723a7252012-10-12 14:26:31 -070036import android.provider.Settings;
Jim Miller955a0162012-06-11 21:06:13 -070037import android.text.TextUtils;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.util.TypedValue;
41import android.view.Gravity;
42import android.view.MotionEvent;
43import android.view.View;
Jim Miller955a0162012-06-11 21:06:13 -070044import android.view.accessibility.AccessibilityManager;
45
46import com.android.internal.R;
47
48import java.util.ArrayList;
49
50/**
51 * A re-usable widget containing a center, outer ring and wave animation.
52 */
53public class GlowPadView extends View {
54 private static final String TAG = "GlowPadView";
55 private static final boolean DEBUG = false;
56
57 // Wave state machine
58 private static final int STATE_IDLE = 0;
59 private static final int STATE_START = 1;
60 private static final int STATE_FIRST_TOUCH = 2;
61 private static final int STATE_TRACKING = 3;
62 private static final int STATE_SNAP = 4;
63 private static final int STATE_FINISH = 5;
64
65 // Animation properties.
66 private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it
67
68 public interface OnTriggerListener {
69 int NO_HANDLE = 0;
70 int CENTER_HANDLE = 1;
71 public void onGrabbed(View v, int handle);
72 public void onReleased(View v, int handle);
73 public void onTrigger(View v, int target);
74 public void onGrabbedStateChange(View v, int handle);
75 public void onFinishFinalAnimation();
76 }
77
78 // Tuneable parameters for animation
Chet Haase167a3232013-10-02 16:11:54 -070079 private static final int WAVE_ANIMATION_DURATION = 1000;
Jim Miller955a0162012-06-11 21:06:13 -070080 private static final int RETURN_TO_HOME_DELAY = 1200;
81 private static final int RETURN_TO_HOME_DURATION = 200;
82 private static final int HIDE_ANIMATION_DELAY = 200;
83 private static final int HIDE_ANIMATION_DURATION = 200;
84 private static final int SHOW_ANIMATION_DURATION = 200;
85 private static final int SHOW_ANIMATION_DELAY = 50;
86 private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
87 private static final int REVEAL_GLOW_DELAY = 0;
88 private static final int REVEAL_GLOW_DURATION = 0;
89
90 private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
91 private static final float TARGET_SCALE_EXPANDED = 1.0f;
92 private static final float TARGET_SCALE_COLLAPSED = 0.8f;
93 private static final float RING_SCALE_EXPANDED = 1.0f;
94 private static final float RING_SCALE_COLLAPSED = 0.5f;
95
96 private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
97 private AnimationBundle mWaveAnimations = new AnimationBundle();
98 private AnimationBundle mTargetAnimations = new AnimationBundle();
99 private AnimationBundle mGlowAnimations = new AnimationBundle();
100 private ArrayList<String> mTargetDescriptions;
101 private ArrayList<String> mDirectionDescriptions;
102 private OnTriggerListener mOnTriggerListener;
103 private TargetDrawable mHandleDrawable;
104 private TargetDrawable mOuterRing;
105 private Vibrator mVibrator;
106
107 private int mFeedbackCount = 3;
108 private int mVibrationDuration = 0;
109 private int mGrabbedState;
110 private int mActiveTarget = -1;
111 private float mGlowRadius;
112 private float mWaveCenterX;
113 private float mWaveCenterY;
114 private int mMaxTargetHeight;
115 private int mMaxTargetWidth;
Adam Cohene41dd0f2012-11-06 23:06:22 -0800116 private float mRingScaleFactor = 1f;
Adam Cohenf988bdf2012-11-07 14:28:23 -0800117 private boolean mAllowScaling;
Jim Miller955a0162012-06-11 21:06:13 -0700118
119 private float mOuterRadius = 0.0f;
Jim Miller955a0162012-06-11 21:06:13 -0700120 private float mSnapMargin = 0.0f;
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400121 private float mFirstItemOffset = 0.0f;
122 private boolean mMagneticTargets = false;
Jim Miller955a0162012-06-11 21:06:13 -0700123 private boolean mDragging;
124 private int mNewTargetResources;
125
126 private class AnimationBundle extends ArrayList<Tweener> {
127 private static final long serialVersionUID = 0xA84D78726F127468L;
128 private boolean mSuspended;
129
130 public void start() {
131 if (mSuspended) return; // ignore attempts to start animations
132 final int count = size();
133 for (int i = 0; i < count; i++) {
134 Tweener anim = get(i);
135 anim.animator.start();
136 }
137 }
138
139 public void cancel() {
140 final int count = size();
141 for (int i = 0; i < count; i++) {
142 Tweener anim = get(i);
143 anim.animator.cancel();
144 }
145 clear();
146 }
147
148 public void stop() {
149 final int count = size();
150 for (int i = 0; i < count; i++) {
151 Tweener anim = get(i);
152 anim.animator.end();
153 }
154 clear();
155 }
156
157 public void setSuspended(boolean suspend) {
158 mSuspended = suspend;
159 }
160 };
161
162 private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
163 public void onAnimationEnd(Animator animator) {
164 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
165 dispatchOnFinishFinalAnimation();
166 }
167 };
168
169 private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
170 public void onAnimationEnd(Animator animator) {
171 ping();
172 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
173 dispatchOnFinishFinalAnimation();
174 }
175 };
176
177 private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
178 public void onAnimationUpdate(ValueAnimator animation) {
179 invalidate();
180 }
181 };
182
183 private boolean mAnimatingTargets;
184 private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
185 public void onAnimationEnd(Animator animator) {
186 if (mNewTargetResources != 0) {
187 internalSetTargetResources(mNewTargetResources);
188 mNewTargetResources = 0;
189 hideTargets(false, false);
190 }
191 mAnimatingTargets = false;
192 }
193 };
194 private int mTargetResourceId;
195 private int mTargetDescriptionsResourceId;
196 private int mDirectionDescriptionsResourceId;
197 private boolean mAlwaysTrackFinger;
198 private int mHorizontalInset;
199 private int mVerticalInset;
200 private int mGravity = Gravity.TOP;
201 private boolean mInitialLayout = true;
202 private Tweener mBackgroundAnimator;
203 private PointCloud mPointCloud;
204 private float mInnerRadius;
Jim Millerb4998842012-09-23 17:18:17 -0700205 private int mPointerId;
Jim Miller955a0162012-06-11 21:06:13 -0700206
207 public GlowPadView(Context context) {
208 this(context, null);
209 }
210
211 public GlowPadView(Context context, AttributeSet attrs) {
212 super(context, attrs);
213 Resources res = context.getResources();
214
215 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
216 mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
217 mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
Jim Miller955a0162012-06-11 21:06:13 -0700218 mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400219 mFirstItemOffset = (float) Math.toRadians(
220 a.getFloat(R.styleable.GlowPadView_firstItemOffset,
221 (float) Math.toDegrees(mFirstItemOffset)));
Jim Miller955a0162012-06-11 21:06:13 -0700222 mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration,
223 mVibrationDuration);
224 mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount,
225 mFeedbackCount);
Adam Cohenf988bdf2012-11-07 14:28:23 -0800226 mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false);
Jim Millera592d222012-06-29 17:41:25 -0700227 TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
228 mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0);
Jim Miller955a0162012-06-11 21:06:13 -0700229 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
230 mOuterRing = new TargetDrawable(res,
231 getResourceId(a, R.styleable.GlowPadView_outerRingDrawable));
232
233 mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400234 mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets);
Jim Miller955a0162012-06-11 21:06:13 -0700235
236 int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800237 Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null;
Jim Miller955a0162012-06-11 21:06:13 -0700238 mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);
239
240 TypedValue outValue = new TypedValue();
241
242 // Read array of target drawables
243 if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
244 internalSetTargetResources(outValue.resourceId);
245 }
246 if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
247 throw new IllegalStateException("Must specify at least one target drawable");
248 }
249
250 // Read array of target descriptions
251 if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
252 final int resourceId = outValue.resourceId;
253 if (resourceId == 0) {
254 throw new IllegalStateException("Must specify target descriptions");
255 }
256 setTargetDescriptionsResourceId(resourceId);
257 }
258
259 // Read array of direction descriptions
260 if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
261 final int resourceId = outValue.resourceId;
262 if (resourceId == 0) {
263 throw new IllegalStateException("Must specify direction descriptions");
264 }
265 setDirectionDescriptionsResourceId(resourceId);
266 }
267
Adam Powell962159a2012-10-23 16:09:29 -0700268 mGravity = a.getInt(R.styleable.GlowPadView_gravity, Gravity.TOP);
Jim Miller955a0162012-06-11 21:06:13 -0700269
Jim Miller955a0162012-06-11 21:06:13 -0700270 a.recycle();
271
272 setVibrateEnabled(mVibrationDuration > 0);
273
274 assignDefaultsIfNeeded();
275
276 mPointCloud = new PointCloud(pointDrawable);
277 mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
278 mPointCloud.glowManager.setRadius(mGlowRadius);
279 }
280
281 private int getResourceId(TypedArray a, int id) {
282 TypedValue tv = a.peekValue(id);
283 return tv == null ? 0 : tv.resourceId;
284 }
285
286 private void dump() {
287 Log.v(TAG, "Outer Radius = " + mOuterRadius);
Jim Miller955a0162012-06-11 21:06:13 -0700288 Log.v(TAG, "SnapMargin = " + mSnapMargin);
289 Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
290 Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
291 Log.v(TAG, "GlowRadius = " + mGlowRadius);
292 Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
293 Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
294 }
295
296 public void suspendAnimations() {
297 mWaveAnimations.setSuspended(true);
298 mTargetAnimations.setSuspended(true);
299 mGlowAnimations.setSuspended(true);
300 }
301
302 public void resumeAnimations() {
303 mWaveAnimations.setSuspended(false);
304 mTargetAnimations.setSuspended(false);
305 mGlowAnimations.setSuspended(false);
306 mWaveAnimations.start();
307 mTargetAnimations.start();
308 mGlowAnimations.start();
309 }
310
311 @Override
312 protected int getSuggestedMinimumWidth() {
313 // View should be large enough to contain the background + handle and
314 // target drawable on either edge.
315 return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
316 }
317
318 @Override
319 protected int getSuggestedMinimumHeight() {
320 // View should be large enough to contain the unlock ring + target and
321 // target drawable on either edge
322 return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
323 }
324
Adam Cohene41dd0f2012-11-06 23:06:22 -0800325 /**
326 * This gets the suggested width accounting for the ring's scale factor.
327 */
328 protected int getScaledSuggestedMinimumWidth() {
329 return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius)
330 + mMaxTargetWidth);
331 }
332
333 /**
334 * This gets the suggested height accounting for the ring's scale factor.
335 */
336 protected int getScaledSuggestedMinimumHeight() {
337 return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius)
338 + mMaxTargetHeight);
339 }
340
Jim Miller955a0162012-06-11 21:06:13 -0700341 private int resolveMeasured(int measureSpec, int desired)
342 {
343 int result = 0;
344 int specSize = MeasureSpec.getSize(measureSpec);
345 switch (MeasureSpec.getMode(measureSpec)) {
346 case MeasureSpec.UNSPECIFIED:
347 result = desired;
348 break;
349 case MeasureSpec.AT_MOST:
350 result = Math.min(specSize, desired);
351 break;
352 case MeasureSpec.EXACTLY:
353 default:
354 result = specSize;
355 }
356 return result;
357 }
358
Jim Miller955a0162012-06-11 21:06:13 -0700359 private void switchToState(int state, float x, float y) {
360 switch (state) {
361 case STATE_IDLE:
362 deactivateTargets();
363 hideGlow(0, 0, 0.0f, null);
364 startBackgroundAnimation(0, 0.0f);
365 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
366 mHandleDrawable.setAlpha(1.0f);
367 break;
368
369 case STATE_START:
370 startBackgroundAnimation(0, 0.0f);
371 break;
372
373 case STATE_FIRST_TOUCH:
374 mHandleDrawable.setAlpha(0.0f);
375 deactivateTargets();
376 showTargets(true);
377 startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
378 setGrabbedState(OnTriggerListener.CENTER_HANDLE);
379 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
380 announceTargets();
381 }
382 break;
383
384 case STATE_TRACKING:
385 mHandleDrawable.setAlpha(0.0f);
386 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null);
387 break;
388
389 case STATE_SNAP:
390 // TODO: Add transition states (see list_selector_background_transition.xml)
391 mHandleDrawable.setAlpha(0.0f);
392 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
393 break;
394
395 case STATE_FINISH:
396 doFinish();
397 break;
398 }
399 }
400
401 private void showGlow(int duration, int delay, float finalAlpha,
402 AnimatorListener finishListener) {
403 mGlowAnimations.cancel();
404 mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
405 "ease", Ease.Cubic.easeIn,
406 "delay", delay,
407 "alpha", finalAlpha,
408 "onUpdate", mUpdateListener,
409 "onComplete", finishListener));
410 mGlowAnimations.start();
411 }
412
413 private void hideGlow(int duration, int delay, float finalAlpha,
414 AnimatorListener finishListener) {
415 mGlowAnimations.cancel();
416 mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
417 "ease", Ease.Quart.easeOut,
418 "delay", delay,
419 "alpha", finalAlpha,
420 "x", 0.0f,
421 "y", 0.0f,
422 "onUpdate", mUpdateListener,
423 "onComplete", finishListener));
424 mGlowAnimations.start();
425 }
426
427 private void deactivateTargets() {
428 final int count = mTargetDrawables.size();
429 for (int i = 0; i < count; i++) {
430 TargetDrawable target = mTargetDrawables.get(i);
431 target.setState(TargetDrawable.STATE_INACTIVE);
432 }
433 mActiveTarget = -1;
434 }
435
436 /**
437 * Dispatches a trigger event to listener. Ignored if a listener is not set.
438 * @param whichTarget the target that was triggered.
439 */
440 private void dispatchTriggerEvent(int whichTarget) {
441 vibrate();
442 if (mOnTriggerListener != null) {
443 mOnTriggerListener.onTrigger(this, whichTarget);
444 }
445 }
446
447 private void dispatchOnFinishFinalAnimation() {
448 if (mOnTriggerListener != null) {
449 mOnTriggerListener.onFinishFinalAnimation();
450 }
451 }
452
453 private void doFinish() {
454 final int activeTarget = mActiveTarget;
455 final boolean targetHit = activeTarget != -1;
456
457 if (targetHit) {
458 if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);
459
460 highlightSelected(activeTarget);
461
462 // Inform listener of any active targets. Typically only one will be active.
463 hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
464 dispatchTriggerEvent(activeTarget);
465 if (!mAlwaysTrackFinger) {
466 // Force ring and targets to finish animation to final expanded state
467 mTargetAnimations.stop();
468 }
469 } else {
470 // Animate handle back to the center based on current state.
471 hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
472 hideTargets(true, false);
473 }
474
475 setGrabbedState(OnTriggerListener.NO_HANDLE);
476 }
477
478 private void highlightSelected(int activeTarget) {
479 // Highlight the given target and fade others
480 mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
481 hideUnselected(activeTarget);
482 }
483
484 private void hideUnselected(int active) {
485 for (int i = 0; i < mTargetDrawables.size(); i++) {
486 if (i != active) {
487 mTargetDrawables.get(i).setAlpha(0.0f);
488 }
489 }
490 }
491
492 private void hideTargets(boolean animate, boolean expanded) {
493 mTargetAnimations.cancel();
494 // Note: these animations should complete at the same time so that we can swap out
495 // the target assets asynchronously from the setTargetResources() call.
496 mAnimatingTargets = animate;
497 final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
498 final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
499
Jim Millera7da8af2012-06-19 16:17:19 -0700500 final float targetScale = expanded ?
Jim Miller5892e2e2012-06-18 17:04:58 -0700501 TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
Jim Miller955a0162012-06-11 21:06:13 -0700502 final int length = mTargetDrawables.size();
503 final TimeInterpolator interpolator = Ease.Cubic.easeOut;
504 for (int i = 0; i < length; i++) {
505 TargetDrawable target = mTargetDrawables.get(i);
506 target.setState(TargetDrawable.STATE_INACTIVE);
507 mTargetAnimations.add(Tweener.to(target, duration,
508 "ease", interpolator,
509 "alpha", 0.0f,
510 "scaleX", targetScale,
511 "scaleY", targetScale,
512 "delay", delay,
513 "onUpdate", mUpdateListener));
514 }
515
Adam Cohene41dd0f2012-11-06 23:06:22 -0800516 float ringScaleTarget = expanded ?
Jim Miller5892e2e2012-06-18 17:04:58 -0700517 RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
Adam Cohene41dd0f2012-11-06 23:06:22 -0800518 ringScaleTarget *= mRingScaleFactor;
Jim Miller955a0162012-06-11 21:06:13 -0700519 mTargetAnimations.add(Tweener.to(mOuterRing, duration,
520 "ease", interpolator,
521 "alpha", 0.0f,
522 "scaleX", ringScaleTarget,
523 "scaleY", ringScaleTarget,
524 "delay", delay,
525 "onUpdate", mUpdateListener,
526 "onComplete", mTargetUpdateListener));
527
528 mTargetAnimations.start();
529 }
530
531 private void showTargets(boolean animate) {
532 mTargetAnimations.stop();
533 mAnimatingTargets = animate;
534 final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
535 final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
536 final int length = mTargetDrawables.size();
537 for (int i = 0; i < length; i++) {
538 TargetDrawable target = mTargetDrawables.get(i);
539 target.setState(TargetDrawable.STATE_INACTIVE);
540 mTargetAnimations.add(Tweener.to(target, duration,
541 "ease", Ease.Cubic.easeOut,
542 "alpha", 1.0f,
543 "scaleX", 1.0f,
544 "scaleY", 1.0f,
545 "delay", delay,
546 "onUpdate", mUpdateListener));
547 }
Adam Cohene41dd0f2012-11-06 23:06:22 -0800548
549 float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED;
Jim Miller955a0162012-06-11 21:06:13 -0700550 mTargetAnimations.add(Tweener.to(mOuterRing, duration,
551 "ease", Ease.Cubic.easeOut,
552 "alpha", 1.0f,
Adam Cohene41dd0f2012-11-06 23:06:22 -0800553 "scaleX", ringScale,
554 "scaleY", ringScale,
Jim Miller955a0162012-06-11 21:06:13 -0700555 "delay", delay,
556 "onUpdate", mUpdateListener,
557 "onComplete", mTargetUpdateListener));
558
559 mTargetAnimations.start();
560 }
561
562 private void vibrate() {
Jeff Sharkey723a7252012-10-12 14:26:31 -0700563 final boolean hapticEnabled = Settings.System.getIntForUser(
564 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
565 UserHandle.USER_CURRENT) != 0;
566 if (mVibrator != null && hapticEnabled) {
Jim Miller955a0162012-06-11 21:06:13 -0700567 mVibrator.vibrate(mVibrationDuration);
568 }
569 }
570
571 private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) {
572 Resources res = getContext().getResources();
573 TypedArray array = res.obtainTypedArray(resourceId);
574 final int count = array.length();
575 ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
576 for (int i = 0; i < count; i++) {
577 TypedValue value = array.peekValue(i);
578 TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0);
579 drawables.add(target);
580 }
581 array.recycle();
582 return drawables;
583 }
584
585 private void internalSetTargetResources(int resourceId) {
586 final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
587 mTargetDrawables = targets;
588 mTargetResourceId = resourceId;
589
590 int maxWidth = mHandleDrawable.getWidth();
591 int maxHeight = mHandleDrawable.getHeight();
592 final int count = targets.size();
593 for (int i = 0; i < count; i++) {
594 TargetDrawable target = targets.get(i);
595 maxWidth = Math.max(maxWidth, target.getWidth());
596 maxHeight = Math.max(maxHeight, target.getHeight());
597 }
598 if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
599 mMaxTargetWidth = maxWidth;
600 mMaxTargetHeight = maxHeight;
601 requestLayout(); // required to resize layout and call updateTargetPositions()
602 } else {
603 updateTargetPositions(mWaveCenterX, mWaveCenterY);
604 updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
605 }
606 }
607
608 /**
609 * Loads an array of drawables from the given resourceId.
610 *
611 * @param resourceId
612 */
613 public void setTargetResources(int resourceId) {
614 if (mAnimatingTargets) {
615 // postpone this change until we return to the initial state
616 mNewTargetResources = resourceId;
617 } else {
618 internalSetTargetResources(resourceId);
619 }
620 }
621
622 public int getTargetResourceId() {
623 return mTargetResourceId;
624 }
625
626 /**
627 * Sets the resource id specifying the target descriptions for accessibility.
628 *
629 * @param resourceId The resource id.
630 */
631 public void setTargetDescriptionsResourceId(int resourceId) {
632 mTargetDescriptionsResourceId = resourceId;
633 if (mTargetDescriptions != null) {
634 mTargetDescriptions.clear();
635 }
636 }
637
638 /**
639 * Gets the resource id specifying the target descriptions for accessibility.
640 *
641 * @return The resource id.
642 */
643 public int getTargetDescriptionsResourceId() {
644 return mTargetDescriptionsResourceId;
645 }
646
647 /**
648 * Sets the resource id specifying the target direction descriptions for accessibility.
649 *
650 * @param resourceId The resource id.
651 */
652 public void setDirectionDescriptionsResourceId(int resourceId) {
653 mDirectionDescriptionsResourceId = resourceId;
654 if (mDirectionDescriptions != null) {
655 mDirectionDescriptions.clear();
656 }
657 }
658
659 /**
660 * Gets the resource id specifying the target direction descriptions.
661 *
662 * @return The resource id.
663 */
664 public int getDirectionDescriptionsResourceId() {
665 return mDirectionDescriptionsResourceId;
666 }
667
668 /**
669 * Enable or disable vibrate on touch.
670 *
671 * @param enabled
672 */
673 public void setVibrateEnabled(boolean enabled) {
674 if (enabled && mVibrator == null) {
675 mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
676 } else {
677 mVibrator = null;
678 }
679 }
680
681 /**
682 * Starts wave animation.
683 *
684 */
685 public void ping() {
686 if (mFeedbackCount > 0) {
Jim Miller5892e2e2012-06-18 17:04:58 -0700687 boolean doWaveAnimation = true;
688 final AnimationBundle waveAnimations = mWaveAnimations;
689
690 // Don't do a wave if there's already one in progress
691 if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
692 long t = waveAnimations.get(0).animator.getCurrentPlayTime();
693 if (t < WAVE_ANIMATION_DURATION/2) {
694 doWaveAnimation = false;
695 }
696 }
697
698 if (doWaveAnimation) {
699 startWaveAnimation();
700 }
Jim Miller955a0162012-06-11 21:06:13 -0700701 }
702 }
703
704 private void stopAndHideWaveAnimation() {
705 mWaveAnimations.cancel();
706 mPointCloud.waveManager.setAlpha(0.0f);
707 }
708
709 private void startWaveAnimation() {
710 mWaveAnimations.cancel();
711 mPointCloud.waveManager.setAlpha(1.0f);
712 mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
713 mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
Jim Miller5892e2e2012-06-18 17:04:58 -0700714 "ease", Ease.Quad.easeOut,
Jim Miller955a0162012-06-11 21:06:13 -0700715 "delay", 0,
716 "radius", 2.0f * mOuterRadius,
717 "onUpdate", mUpdateListener,
718 "onComplete",
719 new AnimatorListenerAdapter() {
720 public void onAnimationEnd(Animator animator) {
721 mPointCloud.waveManager.setRadius(0.0f);
722 mPointCloud.waveManager.setAlpha(0.0f);
723 }
724 }));
725 mWaveAnimations.start();
726 }
727
728 /**
729 * Resets the widget to default state and cancels all animation. If animate is 'true', will
730 * animate objects into place. Otherwise, objects will snap back to place.
731 *
732 * @param animate
733 */
734 public void reset(boolean animate) {
735 mGlowAnimations.stop();
736 mTargetAnimations.stop();
737 startBackgroundAnimation(0, 0.0f);
738 stopAndHideWaveAnimation();
739 hideTargets(animate, false);
Jim Millera592d222012-06-29 17:41:25 -0700740 hideGlow(0, 0, 0.0f, null);
Jim Miller955a0162012-06-11 21:06:13 -0700741 Tweener.reset();
742 }
743
744 private void startBackgroundAnimation(int duration, float alpha) {
745 final Drawable background = getBackground();
746 if (mAlwaysTrackFinger && background != null) {
747 if (mBackgroundAnimator != null) {
748 mBackgroundAnimator.animator.cancel();
749 }
750 mBackgroundAnimator = Tweener.to(background, duration,
751 "ease", Ease.Cubic.easeIn,
752 "alpha", (int)(255.0f * alpha),
753 "delay", SHOW_ANIMATION_DELAY);
754 mBackgroundAnimator.animator.start();
755 }
756 }
757
758 @Override
759 public boolean onTouchEvent(MotionEvent event) {
Jim Millerb4998842012-09-23 17:18:17 -0700760 final int action = event.getActionMasked();
Jim Miller955a0162012-06-11 21:06:13 -0700761 boolean handled = false;
762 switch (action) {
Jim Millerb4998842012-09-23 17:18:17 -0700763 case MotionEvent.ACTION_POINTER_DOWN:
Jim Miller955a0162012-06-11 21:06:13 -0700764 case MotionEvent.ACTION_DOWN:
765 if (DEBUG) Log.v(TAG, "*** DOWN ***");
766 handleDown(event);
767 handleMove(event);
768 handled = true;
769 break;
770
771 case MotionEvent.ACTION_MOVE:
772 if (DEBUG) Log.v(TAG, "*** MOVE ***");
773 handleMove(event);
774 handled = true;
775 break;
776
Jim Millerb4998842012-09-23 17:18:17 -0700777 case MotionEvent.ACTION_POINTER_UP:
Jim Miller955a0162012-06-11 21:06:13 -0700778 case MotionEvent.ACTION_UP:
779 if (DEBUG) Log.v(TAG, "*** UP ***");
780 handleMove(event);
781 handleUp(event);
782 handled = true;
783 break;
784
785 case MotionEvent.ACTION_CANCEL:
786 if (DEBUG) Log.v(TAG, "*** CANCEL ***");
787 handleMove(event);
788 handleCancel(event);
789 handled = true;
790 break;
Jim Millerb4998842012-09-23 17:18:17 -0700791
Jim Miller955a0162012-06-11 21:06:13 -0700792 }
793 invalidate();
794 return handled ? true : super.onTouchEvent(event);
795 }
796
797 private void updateGlowPosition(float x, float y) {
Adam Cohenf988bdf2012-11-07 14:28:23 -0800798 float dx = x - mOuterRing.getX();
799 float dy = y - mOuterRing.getY();
800 dx *= 1f / mRingScaleFactor;
801 dy *= 1f / mRingScaleFactor;
802 mPointCloud.glowManager.setX(mOuterRing.getX() + dx);
803 mPointCloud.glowManager.setY(mOuterRing.getY() + dy);
Jim Miller955a0162012-06-11 21:06:13 -0700804 }
805
806 private void handleDown(MotionEvent event) {
Jim Millerb4998842012-09-23 17:18:17 -0700807 int actionIndex = event.getActionIndex();
808 float eventX = event.getX(actionIndex);
809 float eventY = event.getY(actionIndex);
Jim Miller955a0162012-06-11 21:06:13 -0700810 switchToState(STATE_START, eventX, eventY);
811 if (!trySwitchToFirstTouchState(eventX, eventY)) {
812 mDragging = false;
813 } else {
Jim Millerb4998842012-09-23 17:18:17 -0700814 mPointerId = event.getPointerId(actionIndex);
Jim Miller955a0162012-06-11 21:06:13 -0700815 updateGlowPosition(eventX, eventY);
816 }
817 }
818
819 private void handleUp(MotionEvent event) {
820 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE");
Jim Millerb4998842012-09-23 17:18:17 -0700821 int actionIndex = event.getActionIndex();
822 if (event.getPointerId(actionIndex) == mPointerId) {
823 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
824 }
Jim Miller955a0162012-06-11 21:06:13 -0700825 }
826
827 private void handleCancel(MotionEvent event) {
828 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");
829
Jim Miller245b4532012-10-07 20:16:54 -0700830 // Drop the active target if canceled.
831 mActiveTarget = -1;
Jim Miller955a0162012-06-11 21:06:13 -0700832
Jim Millerb4998842012-09-23 17:18:17 -0700833 int actionIndex = event.findPointerIndex(mPointerId);
834 actionIndex = actionIndex == -1 ? 0 : actionIndex;
835 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
Jim Miller955a0162012-06-11 21:06:13 -0700836 }
837
838 private void handleMove(MotionEvent event) {
839 int activeTarget = -1;
840 final int historySize = event.getHistorySize();
841 ArrayList<TargetDrawable> targets = mTargetDrawables;
842 int ntargets = targets.size();
Jim Miller955a0162012-06-11 21:06:13 -0700843 float x = 0.0f;
844 float y = 0.0f;
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400845 float activeAngle = 0.0f;
Jim Millerb4998842012-09-23 17:18:17 -0700846 int actionIndex = event.findPointerIndex(mPointerId);
847
848 if (actionIndex == -1) {
849 return; // no data for this pointer
850 }
851
Jim Miller955a0162012-06-11 21:06:13 -0700852 for (int k = 0; k < historySize + 1; k++) {
Jim Millerb4998842012-09-23 17:18:17 -0700853 float eventX = k < historySize ? event.getHistoricalX(actionIndex, k)
854 : event.getX(actionIndex);
855 float eventY = k < historySize ? event.getHistoricalY(actionIndex, k)
856 : event.getY(actionIndex);
Jim Miller955a0162012-06-11 21:06:13 -0700857 // tx and ty are relative to wave center
858 float tx = eventX - mWaveCenterX;
859 float ty = eventY - mWaveCenterY;
860 float touchRadius = (float) Math.sqrt(dist2(tx, ty));
861 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
862 float limitX = tx * scale;
863 float limitY = ty * scale;
Michael Jurka53f109bf2012-06-13 17:38:14 -0700864 double angleRad = Math.atan2(-ty, tx);
Jim Miller955a0162012-06-11 21:06:13 -0700865
866 if (!mDragging) {
867 trySwitchToFirstTouchState(eventX, eventY);
868 }
869
870 if (mDragging) {
Michael Jurka53f109bf2012-06-13 17:38:14 -0700871 // For multiple targets, snap to the one that matches
Adam Cohenf988bdf2012-11-07 14:28:23 -0800872 final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
Michael Jurka53f109bf2012-06-13 17:38:14 -0700873 final float snapDistance2 = snapRadius * snapRadius;
874 // Find first target in range
875 for (int i = 0; i < ntargets; i++) {
876 TargetDrawable target = targets.get(i);
877
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400878 double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets;
879 double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets;
Michael Jurka53f109bf2012-06-13 17:38:14 -0700880 if (target.isEnabled()) {
881 boolean angleMatches =
882 (angleRad > targetMinRad && angleRad <= targetMaxRad) ||
883 (angleRad + 2 * Math.PI > targetMinRad &&
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400884 angleRad + 2 * Math.PI <= targetMaxRad) ||
885 (angleRad - 2 * Math.PI > targetMinRad &&
886 angleRad - 2 * Math.PI <= targetMaxRad);
Michael Jurka53f109bf2012-06-13 17:38:14 -0700887 if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
Jim Miller955a0162012-06-11 21:06:13 -0700888 activeTarget = i;
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400889 activeAngle = (float) -angleRad;
Jim Miller955a0162012-06-11 21:06:13 -0700890 }
891 }
892 }
893 }
894 x = limitX;
895 y = limitY;
896 }
897
898 if (!mDragging) {
899 return;
900 }
901
902 if (activeTarget != -1) {
903 switchToState(STATE_SNAP, x,y);
Michael Jurka53f109bf2012-06-13 17:38:14 -0700904 updateGlowPosition(x, y);
Jim Miller955a0162012-06-11 21:06:13 -0700905 } else {
906 switchToState(STATE_TRACKING, x, y);
907 updateGlowPosition(x, y);
908 }
909
910 if (mActiveTarget != activeTarget) {
911 // Defocus the old target
912 if (mActiveTarget != -1) {
913 TargetDrawable target = targets.get(mActiveTarget);
914 if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
915 target.setState(TargetDrawable.STATE_INACTIVE);
916 }
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400917 if (mMagneticTargets) {
918 updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY);
919 }
Jim Miller955a0162012-06-11 21:06:13 -0700920 }
921 // Focus the new target
922 if (activeTarget != -1) {
923 TargetDrawable target = targets.get(activeTarget);
924 if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
925 target.setState(TargetDrawable.STATE_FOCUSED);
926 }
Chris Wrenf0ee5b82012-10-26 17:56:11 -0400927 if (mMagneticTargets) {
928 updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle);
929 }
Jim Miller955a0162012-06-11 21:06:13 -0700930 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
931 String targetContentDescription = getTargetDescription(activeTarget);
alanv78bfb982012-06-20 12:10:48 -0700932 announceForAccessibility(targetContentDescription);
Jim Miller955a0162012-06-11 21:06:13 -0700933 }
934 }
935 }
936 mActiveTarget = activeTarget;
937 }
938
939 @Override
940 public boolean onHoverEvent(MotionEvent event) {
941 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
942 final int action = event.getAction();
943 switch (action) {
944 case MotionEvent.ACTION_HOVER_ENTER:
945 event.setAction(MotionEvent.ACTION_DOWN);
946 break;
947 case MotionEvent.ACTION_HOVER_MOVE:
948 event.setAction(MotionEvent.ACTION_MOVE);
949 break;
950 case MotionEvent.ACTION_HOVER_EXIT:
951 event.setAction(MotionEvent.ACTION_UP);
952 break;
953 }
954 onTouchEvent(event);
955 event.setAction(action);
956 }
Svetoslav Ganov7ce0c132012-11-07 15:54:56 -0800957 super.onHoverEvent(event);
958 return true;
Jim Miller955a0162012-06-11 21:06:13 -0700959 }
960
961 /**
962 * Sets the current grabbed state, and dispatches a grabbed state change
963 * event to our listener.
964 */
965 private void setGrabbedState(int newState) {
966 if (newState != mGrabbedState) {
967 if (newState != OnTriggerListener.NO_HANDLE) {
968 vibrate();
969 }
970 mGrabbedState = newState;
971 if (mOnTriggerListener != null) {
972 if (newState == OnTriggerListener.NO_HANDLE) {
973 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
974 } else {
975 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
976 }
977 mOnTriggerListener.onGrabbedStateChange(this, newState);
978 }
979 }
980 }
981
982 private boolean trySwitchToFirstTouchState(float x, float y) {
983 final float tx = x - mWaveCenterX;
984 final float ty = y - mWaveCenterY;
985 if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) {
986 if (DEBUG) Log.v(TAG, "** Handle HIT");
987 switchToState(STATE_FIRST_TOUCH, x, y);
988 updateGlowPosition(tx, ty);
989 mDragging = true;
990 return true;
991 }
992 return false;
993 }
994
995 private void assignDefaultsIfNeeded() {
996 if (mOuterRadius == 0.0f) {
997 mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f;
998 }
Jim Miller955a0162012-06-11 21:06:13 -0700999 if (mSnapMargin == 0.0f) {
1000 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1001 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
1002 }
1003 if (mInnerRadius == 0.0f) {
1004 mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
1005 }
1006 }
1007
1008 private void computeInsets(int dx, int dy) {
Fabrice Di Meglioe56ffdc2012-09-23 14:51:16 -07001009 final int layoutDirection = getLayoutDirection();
Jim Miller955a0162012-06-11 21:06:13 -07001010 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
1011
1012 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
1013 case Gravity.LEFT:
1014 mHorizontalInset = 0;
1015 break;
1016 case Gravity.RIGHT:
1017 mHorizontalInset = dx;
1018 break;
1019 case Gravity.CENTER_HORIZONTAL:
1020 default:
1021 mHorizontalInset = dx / 2;
1022 break;
1023 }
1024 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
1025 case Gravity.TOP:
1026 mVerticalInset = 0;
1027 break;
1028 case Gravity.BOTTOM:
1029 mVerticalInset = dy;
1030 break;
1031 case Gravity.CENTER_VERTICAL:
1032 default:
1033 mVerticalInset = dy / 2;
1034 break;
1035 }
1036 }
1037
Adam Cohene41dd0f2012-11-06 23:06:22 -08001038 /**
1039 * Given the desired width and height of the ring and the allocated width and height, compute
1040 * how much we need to scale the ring.
1041 */
1042 private float computeScaleFactor(int desiredWidth, int desiredHeight,
1043 int actualWidth, int actualHeight) {
Adam Cohenf988bdf2012-11-07 14:28:23 -08001044
1045 // Return unity if scaling is not allowed.
1046 if (!mAllowScaling) return 1f;
1047
Adam Cohene41dd0f2012-11-06 23:06:22 -08001048 final int layoutDirection = getLayoutDirection();
1049 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
1050
1051 float scaleX = 1f;
1052 float scaleY = 1f;
1053
1054 // We use the gravity as a cue for whether we want to scale on a particular axis.
1055 // We only scale to fit horizontally if we're not pinned to the left or right. Likewise,
1056 // we only scale to fit vertically if we're not pinned to the top or bottom. In these
1057 // cases, we want the ring to hang off the side or top/bottom, respectively.
1058 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
1059 case Gravity.LEFT:
1060 case Gravity.RIGHT:
1061 break;
1062 case Gravity.CENTER_HORIZONTAL:
1063 default:
1064 if (desiredWidth > actualWidth) {
1065 scaleX = (1f * actualWidth - mMaxTargetWidth) /
1066 (desiredWidth - mMaxTargetWidth);
1067 }
1068 break;
1069 }
1070 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
1071 case Gravity.TOP:
1072 case Gravity.BOTTOM:
1073 break;
1074 case Gravity.CENTER_VERTICAL:
1075 default:
1076 if (desiredHeight > actualHeight) {
1077 scaleY = (1f * actualHeight - mMaxTargetHeight) /
1078 (desiredHeight - mMaxTargetHeight);
1079 }
1080 break;
1081 }
1082 return Math.min(scaleX, scaleY);
1083 }
1084
1085 @Override
1086 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1087 final int minimumWidth = getSuggestedMinimumWidth();
1088 final int minimumHeight = getSuggestedMinimumHeight();
1089 int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
1090 int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
1091
1092 mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight,
1093 computedWidth, computedHeight);
1094
1095 int scaledWidth = getScaledSuggestedMinimumWidth();
1096 int scaledHeight = getScaledSuggestedMinimumHeight();
1097
1098 computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight);
1099 setMeasuredDimension(computedWidth, computedHeight);
1100 }
1101
1102 private float getRingWidth() {
1103 return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
1104 }
1105
1106 private float getRingHeight() {
1107 return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
1108 }
1109
Jim Miller955a0162012-06-11 21:06:13 -07001110 @Override
1111 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1112 super.onLayout(changed, left, top, right, bottom);
1113 final int width = right - left;
1114 final int height = bottom - top;
1115
1116 // Target placement width/height. This puts the targets on the greater of the ring
1117 // width or the specified outer radius.
Adam Cohene41dd0f2012-11-06 23:06:22 -08001118 final float placementWidth = getRingWidth();
1119 final float placementHeight = getRingHeight();
Jim Miller955a0162012-06-11 21:06:13 -07001120 float newWaveCenterX = mHorizontalInset
1121 + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
1122 float newWaveCenterY = mVerticalInset
1123 + Math.max(height, + mMaxTargetHeight + placementHeight) / 2;
1124
1125 if (mInitialLayout) {
1126 stopAndHideWaveAnimation();
1127 hideTargets(false, false);
1128 mInitialLayout = false;
1129 }
1130
1131 mOuterRing.setPositionX(newWaveCenterX);
1132 mOuterRing.setPositionY(newWaveCenterY);
1133
Adam Cohene41dd0f2012-11-06 23:06:22 -08001134 mPointCloud.setScale(mRingScaleFactor);
1135
Jim Miller955a0162012-06-11 21:06:13 -07001136 mHandleDrawable.setPositionX(newWaveCenterX);
1137 mHandleDrawable.setPositionY(newWaveCenterY);
1138
1139 updateTargetPositions(newWaveCenterX, newWaveCenterY);
1140 updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
1141 updateGlowPosition(newWaveCenterX, newWaveCenterY);
1142
1143 mWaveCenterX = newWaveCenterX;
1144 mWaveCenterY = newWaveCenterY;
1145
1146 if (DEBUG) dump();
1147 }
1148
Chris Wrenf0ee5b82012-10-26 17:56:11 -04001149 private void updateTargetPosition(int i, float centerX, float centerY) {
1150 final float angle = getAngle(getSliceAngle(), i);
1151 updateTargetPosition(i, centerX, centerY, angle);
1152 }
1153
1154 private void updateTargetPosition(int i, float centerX, float centerY, float angle) {
Adam Cohene41dd0f2012-11-06 23:06:22 -08001155 final float placementRadiusX = getRingWidth() / 2;
1156 final float placementRadiusY = getRingHeight() / 2;
Chris Wrenf0ee5b82012-10-26 17:56:11 -04001157 if (i >= 0) {
1158 ArrayList<TargetDrawable> targets = mTargetDrawables;
Jim Miller955a0162012-06-11 21:06:13 -07001159 final TargetDrawable targetIcon = targets.get(i);
Jim Miller955a0162012-06-11 21:06:13 -07001160 targetIcon.setPositionX(centerX);
1161 targetIcon.setPositionY(centerY);
Adam Cohene41dd0f2012-11-06 23:06:22 -08001162 targetIcon.setX(placementRadiusX * (float) Math.cos(angle));
1163 targetIcon.setY(placementRadiusY * (float) Math.sin(angle));
Jim Miller955a0162012-06-11 21:06:13 -07001164 }
1165 }
1166
Chris Wrenf0ee5b82012-10-26 17:56:11 -04001167 private void updateTargetPositions(float centerX, float centerY) {
1168 updateTargetPositions(centerX, centerY, false);
1169 }
1170
1171 private void updateTargetPositions(float centerX, float centerY, boolean skipActive) {
1172 final int size = mTargetDrawables.size();
1173 final float alpha = getSliceAngle();
1174 // Reposition the target drawables if the view changed.
1175 for (int i = 0; i < size; i++) {
1176 if (!skipActive || i != mActiveTarget) {
1177 updateTargetPosition(i, centerX, centerY, getAngle(alpha, i));
1178 }
1179 }
1180 }
1181
1182 private float getAngle(float alpha, int i) {
1183 return mFirstItemOffset + alpha * i;
1184 }
1185
1186 private float getSliceAngle() {
1187 return (float) (-2.0f * Math.PI / mTargetDrawables.size());
1188 }
1189
Jim Miller955a0162012-06-11 21:06:13 -07001190 private void updatePointCloudPosition(float centerX, float centerY) {
1191 mPointCloud.setCenter(centerX, centerY);
1192 }
1193
1194 @Override
1195 protected void onDraw(Canvas canvas) {
1196 mPointCloud.draw(canvas);
1197 mOuterRing.draw(canvas);
1198 final int ntargets = mTargetDrawables.size();
1199 for (int i = 0; i < ntargets; i++) {
1200 TargetDrawable target = mTargetDrawables.get(i);
1201 if (target != null) {
1202 target.draw(canvas);
1203 }
1204 }
1205 mHandleDrawable.draw(canvas);
1206 }
1207
1208 public void setOnTriggerListener(OnTriggerListener listener) {
1209 mOnTriggerListener = listener;
1210 }
1211
1212 private float square(float d) {
1213 return d * d;
1214 }
1215
1216 private float dist2(float dx, float dy) {
1217 return dx*dx + dy*dy;
1218 }
1219
1220 private float getScaledGlowRadiusSquared() {
1221 final float scaledTapRadius;
1222 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
1223 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
1224 } else {
1225 scaledTapRadius = mGlowRadius;
1226 }
1227 return square(scaledTapRadius);
1228 }
1229
1230 private void announceTargets() {
1231 StringBuilder utterance = new StringBuilder();
1232 final int targetCount = mTargetDrawables.size();
1233 for (int i = 0; i < targetCount; i++) {
1234 String targetDescription = getTargetDescription(i);
1235 String directionDescription = getDirectionDescription(i);
1236 if (!TextUtils.isEmpty(targetDescription)
1237 && !TextUtils.isEmpty(directionDescription)) {
1238 String text = String.format(directionDescription, targetDescription);
1239 utterance.append(text);
1240 }
Jim Miller955a0162012-06-11 21:06:13 -07001241 }
alanv78bfb982012-06-20 12:10:48 -07001242 if (utterance.length() > 0) {
1243 announceForAccessibility(utterance.toString());
1244 }
Jim Miller955a0162012-06-11 21:06:13 -07001245 }
1246
1247 private String getTargetDescription(int index) {
1248 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
1249 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
1250 if (mTargetDrawables.size() != mTargetDescriptions.size()) {
1251 Log.w(TAG, "The number of target drawables must be"
1252 + " equal to the number of target descriptions.");
1253 return null;
1254 }
1255 }
1256 return mTargetDescriptions.get(index);
1257 }
1258
1259 private String getDirectionDescription(int index) {
1260 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
1261 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
1262 if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
1263 Log.w(TAG, "The number of target drawables must be"
1264 + " equal to the number of direction descriptions.");
1265 return null;
1266 }
1267 }
1268 return mDirectionDescriptions.get(index);
1269 }
1270
1271 private ArrayList<String> loadDescriptions(int resourceId) {
1272 TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
1273 final int count = array.length();
1274 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
1275 for (int i = 0; i < count; i++) {
1276 String contentDescription = array.getString(i);
1277 targetContentDescriptions.add(contentDescription);
1278 }
1279 array.recycle();
1280 return targetContentDescriptions;
1281 }
1282
1283 public int getResourceIdForTarget(int index) {
1284 final TargetDrawable drawable = mTargetDrawables.get(index);
1285 return drawable == null ? 0 : drawable.getResourceId();
1286 }
1287
1288 public void setEnableTarget(int resourceId, boolean enabled) {
1289 for (int i = 0; i < mTargetDrawables.size(); i++) {
1290 final TargetDrawable target = mTargetDrawables.get(i);
1291 if (target.getResourceId() == resourceId) {
1292 target.setEnabled(enabled);
1293 break; // should never be more than one match
1294 }
1295 }
1296 }
1297
1298 /**
1299 * Gets the position of a target in the array that matches the given resource.
1300 * @param resourceId
1301 * @return the index or -1 if not found
1302 */
1303 public int getTargetPosition(int resourceId) {
1304 for (int i = 0; i < mTargetDrawables.size(); i++) {
1305 final TargetDrawable target = mTargetDrawables.get(i);
1306 if (target.getResourceId() == resourceId) {
1307 return i; // should never be more than one match
1308 }
1309 }
1310 return -1;
1311 }
1312
1313 private boolean replaceTargetDrawables(Resources res, int existingResourceId,
1314 int newResourceId) {
1315 if (existingResourceId == 0 || newResourceId == 0) {
1316 return false;
1317 }
1318
1319 boolean result = false;
1320 final ArrayList<TargetDrawable> drawables = mTargetDrawables;
1321 final int size = drawables.size();
1322 for (int i = 0; i < size; i++) {
1323 final TargetDrawable target = drawables.get(i);
1324 if (target != null && target.getResourceId() == existingResourceId) {
1325 target.setDrawable(res, newResourceId);
1326 result = true;
1327 }
1328 }
1329
1330 if (result) {
1331 requestLayout(); // in case any given drawable's size changes
1332 }
1333
1334 return result;
1335 }
1336
1337 /**
1338 * Searches the given package for a resource to use to replace the Drawable on the
1339 * target with the given resource id
1340 * @param component of the .apk that contains the resource
1341 * @param name of the metadata in the .apk
1342 * @param existingResId the resource id of the target to search for
1343 * @return true if found in the given package and replaced at least one target Drawables
1344 */
1345 public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name,
1346 int existingResId) {
1347 if (existingResId == 0) return false;
1348
Jim Miller45308b12012-06-18 19:23:39 -07001349 boolean replaced = false;
1350 if (component != null) {
1351 try {
1352 PackageManager packageManager = mContext.getPackageManager();
1353 // Look for the search icon specified in the activity meta-data
1354 Bundle metaData = packageManager.getActivityInfo(
1355 component, PackageManager.GET_META_DATA).metaData;
1356 if (metaData != null) {
1357 int iconResId = metaData.getInt(name);
1358 if (iconResId != 0) {
1359 Resources res = packageManager.getResourcesForActivity(component);
1360 replaced = replaceTargetDrawables(res, existingResId, iconResId);
1361 }
Jim Miller955a0162012-06-11 21:06:13 -07001362 }
Jim Miller45308b12012-06-18 19:23:39 -07001363 } catch (NameNotFoundException e) {
1364 Log.w(TAG, "Failed to swap drawable; "
1365 + component.flattenToShortString() + " not found", e);
1366 } catch (Resources.NotFoundException nfe) {
1367 Log.w(TAG, "Failed to swap drawable from "
1368 + component.flattenToShortString(), nfe);
Jim Miller955a0162012-06-11 21:06:13 -07001369 }
Jim Miller955a0162012-06-11 21:06:13 -07001370 }
Jim Miller45308b12012-06-18 19:23:39 -07001371 if (!replaced) {
1372 // Restore the original drawable
1373 replaceTargetDrawables(mContext.getResources(), existingResId, existingResId);
1374 }
1375 return replaced;
Jim Miller955a0162012-06-11 21:06:13 -07001376 }
1377}