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