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