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