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