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