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