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