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