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