1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.widget.multiwaveview; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.animation.ValueAnimator; 25 import android.animation.ValueAnimator.AnimatorUpdateListener; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.graphics.Canvas; 33 import android.graphics.RectF; 34 import android.graphics.drawable.Drawable; 35 import android.os.Bundle; 36 import android.os.Vibrator; 37 import android.text.TextUtils; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.TypedValue; 41 import android.view.Gravity; 42 import android.view.MotionEvent; 43 import android.view.View; 44 import android.view.accessibility.AccessibilityEvent; 45 import android.view.accessibility.AccessibilityManager; 46 47 import com.android.internal.R; 48 49 import java.util.ArrayList; 50 51 /** 52 * A special widget containing a center and outer ring. Moving the center ring to the outer ring 53 * causes an event that can be caught by implementing OnTriggerListener. 54 */ 55 public class MultiWaveView extends View { 56 private static final String TAG = "MultiWaveView"; 57 private static final boolean DEBUG = false; 58 59 // Wave state machine 60 private static final int STATE_IDLE = 0; 61 private static final int STATE_START = 1; 62 private static final int STATE_FIRST_TOUCH = 2; 63 private static final int STATE_TRACKING = 3; 64 private static final int STATE_SNAP = 4; 65 private static final int STATE_FINISH = 5; 66 67 // Animation properties. 68 private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it 69 70 public interface OnTriggerListener { 71 int NO_HANDLE = 0; 72 int CENTER_HANDLE = 1; onGrabbed(View v, int handle)73 public void onGrabbed(View v, int handle); onReleased(View v, int handle)74 public void onReleased(View v, int handle); onTrigger(View v, int target)75 public void onTrigger(View v, int target); onGrabbedStateChange(View v, int handle)76 public void onGrabbedStateChange(View v, int handle); onFinishFinalAnimation()77 public void onFinishFinalAnimation(); 78 } 79 80 // Tuneable parameters for animation 81 private static final int CHEVRON_INCREMENTAL_DELAY = 160; 82 private static final int CHEVRON_ANIMATION_DURATION = 850; 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 91 private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; 92 private static final float TARGET_SCALE_EXPANDED = 1.0f; 93 private static final float TARGET_SCALE_COLLAPSED = 0.8f; 94 private static final float RING_SCALE_EXPANDED = 1.0f; 95 private static final float RING_SCALE_COLLAPSED = 0.5f; 96 97 private TimeInterpolator mChevronAnimationInterpolator = Ease.Quad.easeOut; 98 99 private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); 100 private ArrayList<TargetDrawable> mChevronDrawables = new ArrayList<TargetDrawable>(); 101 private AnimationBundle mChevronAnimations = new AnimationBundle(); 102 private AnimationBundle mTargetAnimations = new AnimationBundle(); 103 private AnimationBundle mHandleAnimations = new AnimationBundle(); 104 private ArrayList<String> mTargetDescriptions; 105 private ArrayList<String> mDirectionDescriptions; 106 private OnTriggerListener mOnTriggerListener; 107 private TargetDrawable mHandleDrawable; 108 private TargetDrawable mOuterRing; 109 private Vibrator mVibrator; 110 111 private int mFeedbackCount = 3; 112 private int mVibrationDuration = 0; 113 private int mGrabbedState; 114 private int mActiveTarget = -1; 115 private float mTapRadius; 116 private float mWaveCenterX; 117 private float mWaveCenterY; 118 private int mMaxTargetHeight; 119 private int mMaxTargetWidth; 120 121 private float mOuterRadius = 0.0f; 122 private float mSnapMargin = 0.0f; 123 private boolean mDragging; 124 private int mNewTargetResources; 125 126 private class AnimationBundle extends ArrayList<Tweener> { 127 private static final long serialVersionUID = 0xA84D78726F127468L; 128 private boolean mSuspended; 129 start()130 public void start() { 131 if (mSuspended) return; // ignore attempts to start animations 132 final int count = size(); 133 for (int i = 0; i < count; i++) { 134 Tweener anim = get(i); 135 anim.animator.start(); 136 } 137 } 138 cancel()139 public void cancel() { 140 final int count = size(); 141 for (int i = 0; i < count; i++) { 142 Tweener anim = get(i); 143 anim.animator.cancel(); 144 } 145 clear(); 146 } 147 stop()148 public void stop() { 149 final int count = size(); 150 for (int i = 0; i < count; i++) { 151 Tweener anim = get(i); 152 anim.animator.end(); 153 } 154 clear(); 155 } 156 setSuspended(boolean suspend)157 public void setSuspended(boolean suspend) { 158 mSuspended = suspend; 159 } 160 }; 161 162 private AnimatorListener mResetListener = new AnimatorListenerAdapter() { 163 public void onAnimationEnd(Animator animator) { 164 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 165 dispatchOnFinishFinalAnimation(); 166 } 167 }; 168 169 private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { 170 public void onAnimationEnd(Animator animator) { 171 ping(); 172 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 173 dispatchOnFinishFinalAnimation(); 174 } 175 }; 176 177 private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { 178 public void onAnimationUpdate(ValueAnimator animation) { 179 invalidateGlobalRegion(mHandleDrawable); 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 MultiWaveView(Context context)205 public MultiWaveView(Context context) { 206 this(context, null); 207 } 208 MultiWaveView(Context context, AttributeSet attrs)209 public MultiWaveView(Context context, AttributeSet attrs) { 210 super(context, attrs); 211 Resources res = context.getResources(); 212 213 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiWaveView); 214 mOuterRadius = a.getDimension(R.styleable.MultiWaveView_outerRadius, mOuterRadius); 215 mSnapMargin = a.getDimension(R.styleable.MultiWaveView_snapMargin, mSnapMargin); 216 mVibrationDuration = a.getInt(R.styleable.MultiWaveView_vibrationDuration, 217 mVibrationDuration); 218 mFeedbackCount = a.getInt(R.styleable.MultiWaveView_feedbackCount, 219 mFeedbackCount); 220 mHandleDrawable = new TargetDrawable(res, 221 a.peekValue(R.styleable.MultiWaveView_handleDrawable).resourceId); 222 mTapRadius = mHandleDrawable.getWidth()/2; 223 mOuterRing = new TargetDrawable(res, 224 a.peekValue(R.styleable.MultiWaveView_waveDrawable).resourceId); 225 mAlwaysTrackFinger = a.getBoolean(R.styleable.MultiWaveView_alwaysTrackFinger, false); 226 227 // Read array of chevron drawables 228 TypedValue outValue = new TypedValue(); 229 if (a.getValue(R.styleable.MultiWaveView_chevronDrawables, outValue)) { 230 ArrayList<TargetDrawable> chevrons = loadDrawableArray(outValue.resourceId); 231 for (int i = 0; i < chevrons.size(); i++) { 232 final TargetDrawable chevron = chevrons.get(i); 233 for (int k = 0; k < mFeedbackCount; k++) { 234 mChevronDrawables.add(chevron == null ? null : new TargetDrawable(chevron)); 235 } 236 } 237 } 238 239 // Read array of target drawables 240 if (a.getValue(R.styleable.MultiWaveView_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.MultiWaveView_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.MultiWaveView_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, android.R.styleable.LinearLayout); 269 mGravity = a.getInt(android.R.styleable.LinearLayout_gravity, Gravity.TOP); 270 a.recycle(); 271 272 setVibrateEnabled(mVibrationDuration > 0); 273 assignDefaultsIfNeeded(); 274 } 275 dump()276 private void dump() { 277 Log.v(TAG, "Outer Radius = " + mOuterRadius); 278 Log.v(TAG, "SnapMargin = " + mSnapMargin); 279 Log.v(TAG, "FeedbackCount = " + mFeedbackCount); 280 Log.v(TAG, "VibrationDuration = " + mVibrationDuration); 281 Log.v(TAG, "TapRadius = " + mTapRadius); 282 Log.v(TAG, "WaveCenterX = " + mWaveCenterX); 283 Log.v(TAG, "WaveCenterY = " + mWaveCenterY); 284 } 285 suspendAnimations()286 public void suspendAnimations() { 287 mChevronAnimations.setSuspended(true); 288 mTargetAnimations.setSuspended(true); 289 mHandleAnimations.setSuspended(true); 290 } 291 resumeAnimations()292 public void resumeAnimations() { 293 mChevronAnimations.setSuspended(false); 294 mTargetAnimations.setSuspended(false); 295 mHandleAnimations.setSuspended(false); 296 mChevronAnimations.start(); 297 mTargetAnimations.start(); 298 mHandleAnimations.start(); 299 } 300 301 @Override getSuggestedMinimumWidth()302 protected int getSuggestedMinimumWidth() { 303 // View should be large enough to contain the background + handle and 304 // target drawable on either edge. 305 return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); 306 } 307 308 @Override getSuggestedMinimumHeight()309 protected int getSuggestedMinimumHeight() { 310 // View should be large enough to contain the unlock ring + target and 311 // target drawable on either edge 312 return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); 313 } 314 resolveMeasured(int measureSpec, int desired)315 private int resolveMeasured(int measureSpec, int desired) 316 { 317 int result = 0; 318 int specSize = MeasureSpec.getSize(measureSpec); 319 switch (MeasureSpec.getMode(measureSpec)) { 320 case MeasureSpec.UNSPECIFIED: 321 result = desired; 322 break; 323 case MeasureSpec.AT_MOST: 324 result = Math.min(specSize, desired); 325 break; 326 case MeasureSpec.EXACTLY: 327 default: 328 result = specSize; 329 } 330 return result; 331 } 332 333 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)334 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 335 final int minimumWidth = getSuggestedMinimumWidth(); 336 final int minimumHeight = getSuggestedMinimumHeight(); 337 int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 338 int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 339 computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight)); 340 setMeasuredDimension(computedWidth, computedHeight); 341 } 342 switchToState(int state, float x, float y)343 private void switchToState(int state, float x, float y) { 344 switch (state) { 345 case STATE_IDLE: 346 deactivateTargets(); 347 hideTargets(true, false); 348 startBackgroundAnimation(0, 0.0f); 349 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); 350 break; 351 352 case STATE_START: 353 deactivateHandle(0, 0, 1.0f, null); 354 startBackgroundAnimation(0, 0.0f); 355 break; 356 357 case STATE_FIRST_TOUCH: 358 deactivateTargets(); 359 showTargets(true); 360 mHandleDrawable.setState(TargetDrawable.STATE_ACTIVE); 361 startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); 362 setGrabbedState(OnTriggerListener.CENTER_HANDLE); 363 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 364 announceTargets(); 365 } 366 break; 367 368 case STATE_TRACKING: 369 break; 370 371 case STATE_SNAP: 372 break; 373 374 case STATE_FINISH: 375 doFinish(); 376 break; 377 } 378 } 379 activateHandle(int duration, int delay, float finalAlpha, AnimatorListener finishListener)380 private void activateHandle(int duration, int delay, float finalAlpha, 381 AnimatorListener finishListener) { 382 mHandleAnimations.cancel(); 383 mHandleAnimations.add(Tweener.to(mHandleDrawable, duration, 384 "ease", Ease.Cubic.easeIn, 385 "delay", delay, 386 "alpha", finalAlpha, 387 "onUpdate", mUpdateListener, 388 "onComplete", finishListener)); 389 mHandleAnimations.start(); 390 } 391 deactivateHandle(int duration, int delay, float finalAlpha, AnimatorListener finishListener)392 private void deactivateHandle(int duration, int delay, float finalAlpha, 393 AnimatorListener finishListener) { 394 mHandleAnimations.cancel(); 395 mHandleAnimations.add(Tweener.to(mHandleDrawable, duration, 396 "ease", Ease.Quart.easeOut, 397 "delay", delay, 398 "alpha", finalAlpha, 399 "x", 0, 400 "y", 0, 401 "onUpdate", mUpdateListener, 402 "onComplete", finishListener)); 403 mHandleAnimations.start(); 404 } 405 406 /** 407 * Animation used to attract user's attention to the target button. 408 * Assumes mChevronDrawables is an a list with an even number of chevrons filled with 409 * mFeedbackCount items in the order: left, right, top, bottom. 410 */ startChevronAnimation()411 private void startChevronAnimation() { 412 final float chevronStartDistance = mHandleDrawable.getWidth() * 0.8f; 413 final float chevronStopDistance = mOuterRadius * 0.9f / 2.0f; 414 final float startScale = 0.5f; 415 final float endScale = 2.0f; 416 final int directionCount = mFeedbackCount > 0 ? mChevronDrawables.size()/mFeedbackCount : 0; 417 418 mChevronAnimations.stop(); 419 420 // Add an animation for all chevron drawables. There are mFeedbackCount drawables 421 // in each direction and directionCount directions. 422 for (int direction = 0; direction < directionCount; direction++) { 423 double angle = 2.0 * Math.PI * direction / directionCount; 424 final float sx = (float) Math.cos(angle); 425 final float sy = 0.0f - (float) Math.sin(angle); 426 final float[] xrange = new float[] 427 {sx * chevronStartDistance, sx * chevronStopDistance}; 428 final float[] yrange = new float[] 429 {sy * chevronStartDistance, sy * chevronStopDistance}; 430 for (int count = 0; count < mFeedbackCount; count++) { 431 int delay = count * CHEVRON_INCREMENTAL_DELAY; 432 final TargetDrawable icon = mChevronDrawables.get(direction*mFeedbackCount + count); 433 if (icon == null) { 434 continue; 435 } 436 mChevronAnimations.add(Tweener.to(icon, CHEVRON_ANIMATION_DURATION, 437 "ease", mChevronAnimationInterpolator, 438 "delay", delay, 439 "x", xrange, 440 "y", yrange, 441 "alpha", new float[] {1.0f, 0.0f}, 442 "scaleX", new float[] {startScale, endScale}, 443 "scaleY", new float[] {startScale, endScale}, 444 "onUpdate", mUpdateListener)); 445 } 446 } 447 mChevronAnimations.start(); 448 } 449 deactivateTargets()450 private void deactivateTargets() { 451 final int count = mTargetDrawables.size(); 452 for (int i = 0; i < count; i++) { 453 TargetDrawable target = mTargetDrawables.get(i); 454 target.setState(TargetDrawable.STATE_INACTIVE); 455 } 456 mActiveTarget = -1; 457 } 458 invalidateGlobalRegion(TargetDrawable drawable)459 void invalidateGlobalRegion(TargetDrawable drawable) { 460 int width = drawable.getWidth(); 461 int height = drawable.getHeight(); 462 RectF childBounds = new RectF(0, 0, width, height); 463 childBounds.offset(drawable.getX() - width/2, drawable.getY() - height/2); 464 View view = this; 465 while (view.getParent() != null && view.getParent() instanceof View) { 466 view = (View) view.getParent(); 467 view.getMatrix().mapRect(childBounds); 468 view.invalidate((int) Math.floor(childBounds.left), 469 (int) Math.floor(childBounds.top), 470 (int) Math.ceil(childBounds.right), 471 (int) Math.ceil(childBounds.bottom)); 472 } 473 } 474 475 /** 476 * Dispatches a trigger event to listener. Ignored if a listener is not set. 477 * @param whichTarget the target that was triggered. 478 */ dispatchTriggerEvent(int whichTarget)479 private void dispatchTriggerEvent(int whichTarget) { 480 vibrate(); 481 if (mOnTriggerListener != null) { 482 mOnTriggerListener.onTrigger(this, whichTarget); 483 } 484 } 485 dispatchOnFinishFinalAnimation()486 private void dispatchOnFinishFinalAnimation() { 487 if (mOnTriggerListener != null) { 488 mOnTriggerListener.onFinishFinalAnimation(); 489 } 490 } 491 doFinish()492 private void doFinish() { 493 final int activeTarget = mActiveTarget; 494 final boolean targetHit = activeTarget != -1; 495 496 if (targetHit) { 497 if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); 498 499 highlightSelected(activeTarget); 500 501 // Inform listener of any active targets. Typically only one will be active. 502 deactivateHandle(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); 503 dispatchTriggerEvent(activeTarget); 504 if (!mAlwaysTrackFinger) { 505 // Force ring and targets to finish animation to final expanded state 506 mTargetAnimations.stop(); 507 } 508 } else { 509 // Animate handle back to the center based on current state. 510 deactivateHandle(HIDE_ANIMATION_DURATION, HIDE_ANIMATION_DELAY, 1.0f, 511 mResetListenerWithPing); 512 hideTargets(true, false); 513 } 514 515 setGrabbedState(OnTriggerListener.NO_HANDLE); 516 } 517 highlightSelected(int activeTarget)518 private void highlightSelected(int activeTarget) { 519 // Highlight the given target and fade others 520 mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); 521 hideUnselected(activeTarget); 522 } 523 hideUnselected(int active)524 private void hideUnselected(int active) { 525 for (int i = 0; i < mTargetDrawables.size(); i++) { 526 if (i != active) { 527 mTargetDrawables.get(i).setAlpha(0.0f); 528 } 529 } 530 } 531 hideTargets(boolean animate, boolean expanded)532 private void hideTargets(boolean animate, boolean expanded) { 533 mTargetAnimations.cancel(); 534 // Note: these animations should complete at the same time so that we can swap out 535 // the target assets asynchronously from the setTargetResources() call. 536 mAnimatingTargets = animate; 537 final int duration = animate ? HIDE_ANIMATION_DURATION : 0; 538 final int delay = animate ? HIDE_ANIMATION_DELAY : 0; 539 540 final float targetScale = expanded ? TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; 541 final int length = mTargetDrawables.size(); 542 for (int i = 0; i < length; i++) { 543 TargetDrawable target = mTargetDrawables.get(i); 544 target.setState(TargetDrawable.STATE_INACTIVE); 545 mTargetAnimations.add(Tweener.to(target, duration, 546 "ease", Ease.Cubic.easeOut, 547 "alpha", 0.0f, 548 "scaleX", targetScale, 549 "scaleY", targetScale, 550 "delay", delay, 551 "onUpdate", mUpdateListener)); 552 } 553 554 final float ringScaleTarget = expanded ? RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; 555 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 556 "ease", Ease.Cubic.easeOut, 557 "alpha", 0.0f, 558 "scaleX", ringScaleTarget, 559 "scaleY", ringScaleTarget, 560 "delay", delay, 561 "onUpdate", mUpdateListener, 562 "onComplete", mTargetUpdateListener)); 563 564 mTargetAnimations.start(); 565 } 566 showTargets(boolean animate)567 private void showTargets(boolean animate) { 568 mTargetAnimations.stop(); 569 mAnimatingTargets = animate; 570 final int delay = animate ? SHOW_ANIMATION_DELAY : 0; 571 final int duration = animate ? SHOW_ANIMATION_DURATION : 0; 572 final int length = mTargetDrawables.size(); 573 for (int i = 0; i < length; i++) { 574 TargetDrawable target = mTargetDrawables.get(i); 575 target.setState(TargetDrawable.STATE_INACTIVE); 576 mTargetAnimations.add(Tweener.to(target, duration, 577 "ease", Ease.Cubic.easeOut, 578 "alpha", 1.0f, 579 "scaleX", 1.0f, 580 "scaleY", 1.0f, 581 "delay", delay, 582 "onUpdate", mUpdateListener)); 583 } 584 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 585 "ease", Ease.Cubic.easeOut, 586 "alpha", 1.0f, 587 "scaleX", 1.0f, 588 "scaleY", 1.0f, 589 "delay", delay, 590 "onUpdate", mUpdateListener, 591 "onComplete", mTargetUpdateListener)); 592 593 mTargetAnimations.start(); 594 } 595 vibrate()596 private void vibrate() { 597 if (mVibrator != null) { 598 mVibrator.vibrate(mVibrationDuration); 599 } 600 } 601 loadDrawableArray(int resourceId)602 private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { 603 Resources res = getContext().getResources(); 604 TypedArray array = res.obtainTypedArray(resourceId); 605 final int count = array.length(); 606 ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); 607 for (int i = 0; i < count; i++) { 608 TypedValue value = array.peekValue(i); 609 TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); 610 drawables.add(target); 611 } 612 array.recycle(); 613 return drawables; 614 } 615 internalSetTargetResources(int resourceId)616 private void internalSetTargetResources(int resourceId) { 617 mTargetDrawables = loadDrawableArray(resourceId); 618 mTargetResourceId = resourceId; 619 final int count = mTargetDrawables.size(); 620 int maxWidth = mHandleDrawable.getWidth(); 621 int maxHeight = mHandleDrawable.getHeight(); 622 for (int i = 0; i < count; i++) { 623 TargetDrawable target = mTargetDrawables.get(i); 624 maxWidth = Math.max(maxWidth, target.getWidth()); 625 maxHeight = Math.max(maxHeight, target.getHeight()); 626 } 627 if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { 628 mMaxTargetWidth = maxWidth; 629 mMaxTargetHeight = maxHeight; 630 requestLayout(); // required to resize layout and call updateTargetPositions() 631 } else { 632 updateTargetPositions(mWaveCenterX, mWaveCenterY); 633 updateChevronPositions(mWaveCenterX, mWaveCenterY); 634 } 635 } 636 637 /** 638 * Loads an array of drawables from the given resourceId. 639 * 640 * @param resourceId 641 */ setTargetResources(int resourceId)642 public void setTargetResources(int resourceId) { 643 if (mAnimatingTargets) { 644 // postpone this change until we return to the initial state 645 mNewTargetResources = resourceId; 646 } else { 647 internalSetTargetResources(resourceId); 648 } 649 } 650 getTargetResourceId()651 public int getTargetResourceId() { 652 return mTargetResourceId; 653 } 654 655 /** 656 * Sets the resource id specifying the target descriptions for accessibility. 657 * 658 * @param resourceId The resource id. 659 */ setTargetDescriptionsResourceId(int resourceId)660 public void setTargetDescriptionsResourceId(int resourceId) { 661 mTargetDescriptionsResourceId = resourceId; 662 if (mTargetDescriptions != null) { 663 mTargetDescriptions.clear(); 664 } 665 } 666 667 /** 668 * Gets the resource id specifying the target descriptions for accessibility. 669 * 670 * @return The resource id. 671 */ getTargetDescriptionsResourceId()672 public int getTargetDescriptionsResourceId() { 673 return mTargetDescriptionsResourceId; 674 } 675 676 /** 677 * Sets the resource id specifying the target direction descriptions for accessibility. 678 * 679 * @param resourceId The resource id. 680 */ setDirectionDescriptionsResourceId(int resourceId)681 public void setDirectionDescriptionsResourceId(int resourceId) { 682 mDirectionDescriptionsResourceId = resourceId; 683 if (mDirectionDescriptions != null) { 684 mDirectionDescriptions.clear(); 685 } 686 } 687 688 /** 689 * Gets the resource id specifying the target direction descriptions. 690 * 691 * @return The resource id. 692 */ getDirectionDescriptionsResourceId()693 public int getDirectionDescriptionsResourceId() { 694 return mDirectionDescriptionsResourceId; 695 } 696 697 /** 698 * Enable or disable vibrate on touch. 699 * 700 * @param enabled 701 */ setVibrateEnabled(boolean enabled)702 public void setVibrateEnabled(boolean enabled) { 703 if (enabled && mVibrator == null) { 704 mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); 705 } else { 706 mVibrator = null; 707 } 708 } 709 710 /** 711 * Starts chevron animation. Example use case: show chevron animation whenever the phone rings 712 * or the user touches the screen. 713 * 714 */ ping()715 public void ping() { 716 startChevronAnimation(); 717 } 718 719 /** 720 * Resets the widget to default state and cancels all animation. If animate is 'true', will 721 * animate objects into place. Otherwise, objects will snap back to place. 722 * 723 * @param animate 724 */ reset(boolean animate)725 public void reset(boolean animate) { 726 mChevronAnimations.stop(); 727 mHandleAnimations.stop(); 728 mTargetAnimations.stop(); 729 startBackgroundAnimation(0, 0.0f); 730 hideChevrons(); 731 hideTargets(animate, false); 732 deactivateHandle(0, 0, 1.0f, null); 733 Tweener.reset(); 734 } 735 startBackgroundAnimation(int duration, float alpha)736 private void startBackgroundAnimation(int duration, float alpha) { 737 Drawable background = getBackground(); 738 if (mAlwaysTrackFinger && background != null) { 739 if (mBackgroundAnimator != null) { 740 mBackgroundAnimator.animator.end(); 741 } 742 mBackgroundAnimator = Tweener.to(background, duration, 743 "ease", Ease.Cubic.easeIn, 744 "alpha", new int[] {0, (int)(255.0f * alpha)}, 745 "delay", SHOW_ANIMATION_DELAY); 746 mBackgroundAnimator.animator.start(); 747 } 748 } 749 750 @Override onTouchEvent(MotionEvent event)751 public boolean onTouchEvent(MotionEvent event) { 752 final int action = event.getAction(); 753 boolean handled = false; 754 switch (action) { 755 case MotionEvent.ACTION_DOWN: 756 if (DEBUG) Log.v(TAG, "*** DOWN ***"); 757 handleDown(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_UP: 768 if (DEBUG) Log.v(TAG, "*** UP ***"); 769 handleMove(event); 770 handleUp(event); 771 handled = true; 772 break; 773 774 case MotionEvent.ACTION_CANCEL: 775 if (DEBUG) Log.v(TAG, "*** CANCEL ***"); 776 handleMove(event); 777 handleCancel(event); 778 handled = true; 779 break; 780 } 781 invalidate(); 782 return handled ? true : super.onTouchEvent(event); 783 } 784 moveHandleTo(float x, float y, boolean animate)785 private void moveHandleTo(float x, float y, boolean animate) { 786 mHandleDrawable.setX(x); 787 mHandleDrawable.setY(y); 788 } 789 handleDown(MotionEvent event)790 private void handleDown(MotionEvent event) { 791 float eventX = event.getX(); 792 float eventY = event.getY(); 793 switchToState(STATE_START, eventX, eventY); 794 if (!trySwitchToFirstTouchState(eventX, eventY)) { 795 mDragging = false; 796 ping(); 797 } 798 } 799 handleUp(MotionEvent event)800 private void handleUp(MotionEvent event) { 801 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); 802 switchToState(STATE_FINISH, event.getX(), event.getY()); 803 } 804 handleCancel(MotionEvent event)805 private void handleCancel(MotionEvent event) { 806 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); 807 808 // We should drop the active target here but it interferes with 809 // moving off the screen in the direction of the navigation bar. At some point we may 810 // want to revisit how we handle this. For now we'll allow a canceled event to 811 // activate the current target. 812 813 // mActiveTarget = -1; // Drop the active target if canceled. 814 815 switchToState(STATE_FINISH, event.getX(), event.getY()); 816 } 817 handleMove(MotionEvent event)818 private void handleMove(MotionEvent event) { 819 int activeTarget = -1; 820 final int historySize = event.getHistorySize(); 821 ArrayList<TargetDrawable> targets = mTargetDrawables; 822 int ntargets = targets.size(); 823 float x = 0.0f; 824 float y = 0.0f; 825 for (int k = 0; k < historySize + 1; k++) { 826 float eventX = k < historySize ? event.getHistoricalX(k) : event.getX(); 827 float eventY = k < historySize ? event.getHistoricalY(k) : event.getY(); 828 // tx and ty are relative to wave center 829 float tx = eventX - mWaveCenterX; 830 float ty = eventY - mWaveCenterY; 831 float touchRadius = (float) Math.sqrt(dist2(tx, ty)); 832 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; 833 float limitX = tx * scale; 834 float limitY = ty * scale; 835 double angleRad = Math.atan2(-ty, tx); 836 837 if (!mDragging) { 838 trySwitchToFirstTouchState(eventX, eventY); 839 } 840 841 if (mDragging) { 842 // For multiple targets, snap to the one that matches 843 final float snapRadius = mOuterRadius - mSnapMargin; 844 final float snapDistance2 = snapRadius * snapRadius; 845 // Find first target in range 846 for (int i = 0; i < ntargets; i++) { 847 TargetDrawable target = targets.get(i); 848 849 double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; 850 double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; 851 if (target.isEnabled()) { 852 boolean angleMatches = 853 (angleRad > targetMinRad && angleRad <= targetMaxRad) || 854 (angleRad + 2 * Math.PI > targetMinRad && 855 angleRad + 2 * Math.PI <= targetMaxRad); 856 if (angleMatches && (dist2(tx, ty) > snapDistance2)) { 857 activeTarget = i; 858 } 859 } 860 } 861 } 862 x = limitX; 863 y = limitY; 864 } 865 866 if (!mDragging) { 867 return; 868 } 869 870 if (activeTarget != -1) { 871 switchToState(STATE_SNAP, x,y); 872 moveHandleTo(x, y, false); 873 } else { 874 switchToState(STATE_TRACKING, x, y); 875 moveHandleTo(x, y, false); 876 } 877 878 // Draw handle outside parent's bounds 879 invalidateGlobalRegion(mHandleDrawable); 880 881 if (mActiveTarget != activeTarget) { 882 // Defocus the old target 883 if (mActiveTarget != -1) { 884 TargetDrawable target = targets.get(mActiveTarget); 885 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 886 target.setState(TargetDrawable.STATE_INACTIVE); 887 } 888 } 889 // Focus the new target 890 if (activeTarget != -1) { 891 TargetDrawable target = targets.get(activeTarget); 892 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 893 target.setState(TargetDrawable.STATE_FOCUSED); 894 } 895 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 896 String targetContentDescription = getTargetDescription(activeTarget); 897 announceText(targetContentDescription); 898 } 899 activateHandle(0, 0, 0.0f, null); 900 } else { 901 activateHandle(0, 0, 1.0f, null); 902 } 903 } 904 mActiveTarget = activeTarget; 905 } 906 907 @Override 908 public boolean onHoverEvent(MotionEvent event) { 909 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 910 final int action = event.getAction(); 911 switch (action) { 912 case MotionEvent.ACTION_HOVER_ENTER: 913 event.setAction(MotionEvent.ACTION_DOWN); 914 break; 915 case MotionEvent.ACTION_HOVER_MOVE: 916 event.setAction(MotionEvent.ACTION_MOVE); 917 break; 918 case MotionEvent.ACTION_HOVER_EXIT: 919 event.setAction(MotionEvent.ACTION_UP); 920 break; 921 } 922 onTouchEvent(event); 923 event.setAction(action); 924 } 925 return super.onHoverEvent(event); 926 } 927 928 /** 929 * Sets the current grabbed state, and dispatches a grabbed state change 930 * event to our listener. 931 */ 932 private void setGrabbedState(int newState) { 933 if (newState != mGrabbedState) { 934 if (newState != OnTriggerListener.NO_HANDLE) { 935 vibrate(); 936 } 937 mGrabbedState = newState; 938 if (mOnTriggerListener != null) { 939 if (newState == OnTriggerListener.NO_HANDLE) { 940 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); 941 } else { 942 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); 943 } 944 mOnTriggerListener.onGrabbedStateChange(this, newState); 945 } 946 } 947 } 948 949 private boolean trySwitchToFirstTouchState(float x, float y) { 950 final float tx = x - mWaveCenterX; 951 final float ty = y - mWaveCenterY; 952 if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledTapRadiusSquared()) { 953 if (DEBUG) Log.v(TAG, "** Handle HIT"); 954 switchToState(STATE_FIRST_TOUCH, x, y); 955 moveHandleTo(tx, ty, false); 956 mDragging = true; 957 return true; 958 } 959 return false; 960 } 961 962 private void assignDefaultsIfNeeded() { 963 if (mOuterRadius == 0.0f) { 964 mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; 965 } 966 if (mSnapMargin == 0.0f) { 967 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 968 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); 969 } 970 } 971 972 private void computeInsets(int dx, int dy) { 973 final int layoutDirection = getResolvedLayoutDirection(); 974 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 975 976 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 977 case Gravity.LEFT: 978 mHorizontalInset = 0; 979 break; 980 case Gravity.RIGHT: 981 mHorizontalInset = dx; 982 break; 983 case Gravity.CENTER_HORIZONTAL: 984 default: 985 mHorizontalInset = dx / 2; 986 break; 987 } 988 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 989 case Gravity.TOP: 990 mVerticalInset = 0; 991 break; 992 case Gravity.BOTTOM: 993 mVerticalInset = dy; 994 break; 995 case Gravity.CENTER_VERTICAL: 996 default: 997 mVerticalInset = dy / 2; 998 break; 999 } 1000 } 1001 1002 @Override 1003 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1004 super.onLayout(changed, left, top, right, bottom); 1005 final int width = right - left; 1006 final int height = bottom - top; 1007 1008 // Target placement width/height. This puts the targets on the greater of the ring 1009 // width or the specified outer radius. 1010 final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); 1011 final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); 1012 float newWaveCenterX = mHorizontalInset 1013 + Math.max(width, mMaxTargetWidth + placementWidth) / 2; 1014 float newWaveCenterY = mVerticalInset 1015 + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; 1016 1017 if (mInitialLayout) { 1018 hideChevrons(); 1019 hideTargets(false, false); 1020 moveHandleTo(0, 0, false); 1021 mInitialLayout = false; 1022 } 1023 1024 mOuterRing.setPositionX(newWaveCenterX); 1025 mOuterRing.setPositionY(newWaveCenterY); 1026 1027 mHandleDrawable.setPositionX(newWaveCenterX); 1028 mHandleDrawable.setPositionY(newWaveCenterY); 1029 1030 updateTargetPositions(newWaveCenterX, newWaveCenterY); 1031 updateChevronPositions(newWaveCenterX, newWaveCenterY); 1032 1033 mWaveCenterX = newWaveCenterX; 1034 mWaveCenterY = newWaveCenterY; 1035 1036 if (DEBUG) dump(); 1037 } 1038 1039 private void updateTargetPositions(float centerX, float centerY) { 1040 // Reposition the target drawables if the view changed. 1041 ArrayList<TargetDrawable> targets = mTargetDrawables; 1042 final int size = targets.size(); 1043 final float alpha = (float) (-2.0f * Math.PI / size); 1044 for (int i = 0; i < size; i++) { 1045 final TargetDrawable targetIcon = targets.get(i); 1046 final float angle = alpha * i; 1047 targetIcon.setPositionX(centerX); 1048 targetIcon.setPositionY(centerY); 1049 targetIcon.setX(mOuterRadius * (float) Math.cos(angle)); 1050 targetIcon.setY(mOuterRadius * (float) Math.sin(angle)); 1051 } 1052 } 1053 1054 private void updateChevronPositions(float centerX, float centerY) { 1055 ArrayList<TargetDrawable> chevrons = mChevronDrawables; 1056 final int size = chevrons.size(); 1057 for (int i = 0; i < size; i++) { 1058 TargetDrawable target = chevrons.get(i); 1059 if (target != null) { 1060 target.setPositionX(centerX); 1061 target.setPositionY(centerY); 1062 } 1063 } 1064 } 1065 1066 private void hideChevrons() { 1067 ArrayList<TargetDrawable> chevrons = mChevronDrawables; 1068 final int size = chevrons.size(); 1069 for (int i = 0; i < size; i++) { 1070 TargetDrawable chevron = chevrons.get(i); 1071 if (chevron != null) { 1072 chevron.setAlpha(0.0f); 1073 } 1074 } 1075 } 1076 1077 @Override 1078 protected void onDraw(Canvas canvas) { 1079 mOuterRing.draw(canvas); 1080 final int ntargets = mTargetDrawables.size(); 1081 for (int i = 0; i < ntargets; i++) { 1082 TargetDrawable target = mTargetDrawables.get(i); 1083 if (target != null) { 1084 target.draw(canvas); 1085 } 1086 } 1087 final int nchevrons = mChevronDrawables.size(); 1088 for (int i = 0; i < nchevrons; i++) { 1089 TargetDrawable chevron = mChevronDrawables.get(i); 1090 if (chevron != null) { 1091 chevron.draw(canvas); 1092 } 1093 } 1094 mHandleDrawable.draw(canvas); 1095 } 1096 1097 public void setOnTriggerListener(OnTriggerListener listener) { 1098 mOnTriggerListener = listener; 1099 } 1100 1101 private float square(float d) { 1102 return d * d; 1103 } 1104 1105 private float dist2(float dx, float dy) { 1106 return dx*dx + dy*dy; 1107 } 1108 1109 private float getScaledTapRadiusSquared() { 1110 final float scaledTapRadius; 1111 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 1112 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mTapRadius; 1113 } else { 1114 scaledTapRadius = mTapRadius; 1115 } 1116 return square(scaledTapRadius); 1117 } 1118 1119 private void announceTargets() { 1120 StringBuilder utterance = new StringBuilder(); 1121 final int targetCount = mTargetDrawables.size(); 1122 for (int i = 0; i < targetCount; i++) { 1123 String targetDescription = getTargetDescription(i); 1124 String directionDescription = getDirectionDescription(i); 1125 if (!TextUtils.isEmpty(targetDescription) 1126 && !TextUtils.isEmpty(directionDescription)) { 1127 String text = String.format(directionDescription, targetDescription); 1128 utterance.append(text); 1129 } 1130 if (utterance.length() > 0) { 1131 announceText(utterance.toString()); 1132 } 1133 } 1134 } 1135 1136 private void announceText(String text) { 1137 setContentDescription(text); 1138 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1139 setContentDescription(null); 1140 } 1141 1142 private String getTargetDescription(int index) { 1143 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { 1144 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); 1145 if (mTargetDrawables.size() != mTargetDescriptions.size()) { 1146 Log.w(TAG, "The number of target drawables must be" 1147 + " euqal to the number of target descriptions."); 1148 return null; 1149 } 1150 } 1151 return mTargetDescriptions.get(index); 1152 } 1153 1154 private String getDirectionDescription(int index) { 1155 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { 1156 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); 1157 if (mTargetDrawables.size() != mDirectionDescriptions.size()) { 1158 Log.w(TAG, "The number of target drawables must be" 1159 + " euqal to the number of direction descriptions."); 1160 return null; 1161 } 1162 } 1163 return mDirectionDescriptions.get(index); 1164 } 1165 1166 private ArrayList<String> loadDescriptions(int resourceId) { 1167 TypedArray array = getContext().getResources().obtainTypedArray(resourceId); 1168 final int count = array.length(); 1169 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); 1170 for (int i = 0; i < count; i++) { 1171 String contentDescription = array.getString(i); 1172 targetContentDescriptions.add(contentDescription); 1173 } 1174 array.recycle(); 1175 return targetContentDescriptions; 1176 } 1177 1178 public int getResourceIdForTarget(int index) { 1179 final TargetDrawable drawable = mTargetDrawables.get(index); 1180 return drawable == null ? 0 : drawable.getResourceId(); 1181 } 1182 1183 public void setEnableTarget(int resourceId, boolean enabled) { 1184 for (int i = 0; i < mTargetDrawables.size(); i++) { 1185 final TargetDrawable target = mTargetDrawables.get(i); 1186 if (target.getResourceId() == resourceId) { 1187 target.setEnabled(enabled); 1188 break; // should never be more than one match 1189 } 1190 } 1191 } 1192 1193 /** 1194 * Gets the position of a target in the array that matches the given resource. 1195 * @param resourceId 1196 * @return the index or -1 if not found 1197 */ 1198 public int getTargetPosition(int resourceId) { 1199 for (int i = 0; i < mTargetDrawables.size(); i++) { 1200 final TargetDrawable target = mTargetDrawables.get(i); 1201 if (target.getResourceId() == resourceId) { 1202 return i; // should never be more than one match 1203 } 1204 } 1205 return -1; 1206 } 1207 1208 private boolean replaceTargetDrawables(Resources res, int existingResourceId, 1209 int newResourceId) { 1210 if (existingResourceId == 0 || newResourceId == 0) { 1211 return false; 1212 } 1213 1214 boolean result = false; 1215 final ArrayList<TargetDrawable> drawables = mTargetDrawables; 1216 final int size = drawables.size(); 1217 for (int i = 0; i < size; i++) { 1218 final TargetDrawable target = drawables.get(i); 1219 if (target != null && target.getResourceId() == existingResourceId) { 1220 target.setDrawable(res, newResourceId); 1221 result = true; 1222 } 1223 } 1224 1225 if (result) { 1226 requestLayout(); // in case any given drawable's size changes 1227 } 1228 1229 return result; 1230 } 1231 1232 /** 1233 * Searches the given package for a resource to use to replace the Drawable on the 1234 * target with the given resource id 1235 * @param component of the .apk that contains the resource 1236 * @param name of the metadata in the .apk 1237 * @param existingResId the resource id of the target to search for 1238 * @return true if found in the given package and replaced at least one target Drawables 1239 */ 1240 public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, 1241 int existingResId) { 1242 if (existingResId == 0) return false; 1243 1244 try { 1245 PackageManager packageManager = mContext.getPackageManager(); 1246 // Look for the search icon specified in the activity meta-data 1247 Bundle metaData = packageManager.getActivityInfo( 1248 component, PackageManager.GET_META_DATA).metaData; 1249 if (metaData != null) { 1250 int iconResId = metaData.getInt(name); 1251 if (iconResId != 0) { 1252 Resources res = packageManager.getResourcesForActivity(component); 1253 return replaceTargetDrawables(res, existingResId, iconResId); 1254 } 1255 } 1256 } catch (NameNotFoundException e) { 1257 Log.w(TAG, "Failed to swap drawable; " 1258 + component.flattenToShortString() + " not found", e); 1259 } catch (Resources.NotFoundException nfe) { 1260 Log.w(TAG, "Failed to swap drawable from " 1261 + component.flattenToShortString(), nfe); 1262 } 1263 return false; 1264 } 1265 } 1266