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