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