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