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