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