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