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