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