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