GlowPadView.java revision 5892e2ec253465a46b346fc813a21b412ae85e2e
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 ping(); 365 startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); 366 setGrabbedState(OnTriggerListener.CENTER_HANDLE); 367 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 368 announceTargets(); 369 } 370 break; 371 372 case STATE_TRACKING: 373 mHandleDrawable.setAlpha(0.0f); 374 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null); 375 break; 376 377 case STATE_SNAP: 378 // TODO: Add transition states (see list_selector_background_transition.xml) 379 mHandleDrawable.setAlpha(0.0f); 380 showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null); 381 break; 382 383 case STATE_FINISH: 384 doFinish(); 385 break; 386 } 387 } 388 389 private void showGlow(int duration, int delay, float finalAlpha, 390 AnimatorListener finishListener) { 391 mGlowAnimations.cancel(); 392 mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, 393 "ease", Ease.Cubic.easeIn, 394 "delay", delay, 395 "alpha", finalAlpha, 396 "onUpdate", mUpdateListener, 397 "onComplete", finishListener)); 398 mGlowAnimations.start(); 399 } 400 401 private void hideGlow(int duration, int delay, float finalAlpha, 402 AnimatorListener finishListener) { 403 mGlowAnimations.cancel(); 404 mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, 405 "ease", Ease.Quart.easeOut, 406 "delay", delay, 407 "alpha", finalAlpha, 408 "x", 0.0f, 409 "y", 0.0f, 410 "onUpdate", mUpdateListener, 411 "onComplete", finishListener)); 412 mGlowAnimations.start(); 413 } 414 415 private void deactivateTargets() { 416 final int count = mTargetDrawables.size(); 417 for (int i = 0; i < count; i++) { 418 TargetDrawable target = mTargetDrawables.get(i); 419 target.setState(TargetDrawable.STATE_INACTIVE); 420 } 421 mActiveTarget = -1; 422 } 423 424 /** 425 * Dispatches a trigger event to listener. Ignored if a listener is not set. 426 * @param whichTarget the target that was triggered. 427 */ 428 private void dispatchTriggerEvent(int whichTarget) { 429 vibrate(); 430 if (mOnTriggerListener != null) { 431 mOnTriggerListener.onTrigger(this, whichTarget); 432 } 433 } 434 435 private void dispatchOnFinishFinalAnimation() { 436 if (mOnTriggerListener != null) { 437 mOnTriggerListener.onFinishFinalAnimation(); 438 } 439 } 440 441 private void doFinish() { 442 final int activeTarget = mActiveTarget; 443 final boolean targetHit = activeTarget != -1; 444 445 if (targetHit) { 446 if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); 447 448 highlightSelected(activeTarget); 449 450 // Inform listener of any active targets. Typically only one will be active. 451 hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); 452 dispatchTriggerEvent(activeTarget); 453 if (!mAlwaysTrackFinger) { 454 // Force ring and targets to finish animation to final expanded state 455 mTargetAnimations.stop(); 456 } 457 } else { 458 // Animate handle back to the center based on current state. 459 hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing); 460 hideTargets(true, false); 461 } 462 463 setGrabbedState(OnTriggerListener.NO_HANDLE); 464 } 465 466 private void highlightSelected(int activeTarget) { 467 // Highlight the given target and fade others 468 mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); 469 hideUnselected(activeTarget); 470 } 471 472 private void hideUnselected(int active) { 473 for (int i = 0; i < mTargetDrawables.size(); i++) { 474 if (i != active) { 475 mTargetDrawables.get(i).setAlpha(0.0f); 476 } 477 } 478 } 479 480 private void hideTargets(boolean animate, boolean expanded) { 481 mTargetAnimations.cancel(); 482 // Note: these animations should complete at the same time so that we can swap out 483 // the target assets asynchronously from the setTargetResources() call. 484 mAnimatingTargets = animate; 485 final int duration = animate ? HIDE_ANIMATION_DURATION : 0; 486 final int delay = animate ? HIDE_ANIMATION_DELAY : 0; 487 488 // TODO: add an attribute for this. For now we'll show the expand for navbar, but not 489 // keyguard. 490 final boolean expandDisabled = !mAlwaysTrackFinger; 491 492 final float targetScale = (expanded || expandDisabled) ? 493 TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; 494 final int length = mTargetDrawables.size(); 495 final TimeInterpolator interpolator = Ease.Cubic.easeOut; 496 for (int i = 0; i < length; i++) { 497 TargetDrawable target = mTargetDrawables.get(i); 498 target.setState(TargetDrawable.STATE_INACTIVE); 499 mTargetAnimations.add(Tweener.to(target, duration, 500 "ease", interpolator, 501 "alpha", 0.0f, 502 "scaleX", targetScale, 503 "scaleY", targetScale, 504 "delay", delay, 505 "onUpdate", mUpdateListener)); 506 } 507 508 final float ringScaleTarget = (expanded || expandDisabled) ? 509 RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; 510 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 511 "ease", interpolator, 512 "alpha", 0.0f, 513 "scaleX", ringScaleTarget, 514 "scaleY", ringScaleTarget, 515 "delay", delay, 516 "onUpdate", mUpdateListener, 517 "onComplete", mTargetUpdateListener)); 518 519 mTargetAnimations.start(); 520 } 521 522 private void showTargets(boolean animate) { 523 mTargetAnimations.stop(); 524 mAnimatingTargets = animate; 525 final int delay = animate ? SHOW_ANIMATION_DELAY : 0; 526 final int duration = animate ? SHOW_ANIMATION_DURATION : 0; 527 final int length = mTargetDrawables.size(); 528 for (int i = 0; i < length; i++) { 529 TargetDrawable target = mTargetDrawables.get(i); 530 target.setState(TargetDrawable.STATE_INACTIVE); 531 mTargetAnimations.add(Tweener.to(target, duration, 532 "ease", Ease.Cubic.easeOut, 533 "alpha", 1.0f, 534 "scaleX", 1.0f, 535 "scaleY", 1.0f, 536 "delay", delay, 537 "onUpdate", mUpdateListener)); 538 } 539 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 540 "ease", Ease.Cubic.easeOut, 541 "alpha", 1.0f, 542 "scaleX", 1.0f, 543 "scaleY", 1.0f, 544 "delay", delay, 545 "onUpdate", mUpdateListener, 546 "onComplete", mTargetUpdateListener)); 547 548 mTargetAnimations.start(); 549 } 550 551 private void vibrate() { 552 if (mVibrator != null) { 553 mVibrator.vibrate(mVibrationDuration); 554 } 555 } 556 557 private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { 558 Resources res = getContext().getResources(); 559 TypedArray array = res.obtainTypedArray(resourceId); 560 final int count = array.length(); 561 ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); 562 for (int i = 0; i < count; i++) { 563 TypedValue value = array.peekValue(i); 564 TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); 565 drawables.add(target); 566 } 567 array.recycle(); 568 return drawables; 569 } 570 571 private void internalSetTargetResources(int resourceId) { 572 final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId); 573 mTargetDrawables = targets; 574 mTargetResourceId = resourceId; 575 576 int maxWidth = mHandleDrawable.getWidth(); 577 int maxHeight = mHandleDrawable.getHeight(); 578 final int count = targets.size(); 579 for (int i = 0; i < count; i++) { 580 TargetDrawable target = targets.get(i); 581 maxWidth = Math.max(maxWidth, target.getWidth()); 582 maxHeight = Math.max(maxHeight, target.getHeight()); 583 } 584 if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { 585 mMaxTargetWidth = maxWidth; 586 mMaxTargetHeight = maxHeight; 587 requestLayout(); // required to resize layout and call updateTargetPositions() 588 } else { 589 updateTargetPositions(mWaveCenterX, mWaveCenterY); 590 updatePointCloudPosition(mWaveCenterX, mWaveCenterY); 591 } 592 } 593 594 /** 595 * Loads an array of drawables from the given resourceId. 596 * 597 * @param resourceId 598 */ 599 public void setTargetResources(int resourceId) { 600 if (mAnimatingTargets) { 601 // postpone this change until we return to the initial state 602 mNewTargetResources = resourceId; 603 } else { 604 internalSetTargetResources(resourceId); 605 } 606 } 607 608 public int getTargetResourceId() { 609 return mTargetResourceId; 610 } 611 612 /** 613 * Sets the resource id specifying the target descriptions for accessibility. 614 * 615 * @param resourceId The resource id. 616 */ 617 public void setTargetDescriptionsResourceId(int resourceId) { 618 mTargetDescriptionsResourceId = resourceId; 619 if (mTargetDescriptions != null) { 620 mTargetDescriptions.clear(); 621 } 622 } 623 624 /** 625 * Gets the resource id specifying the target descriptions for accessibility. 626 * 627 * @return The resource id. 628 */ 629 public int getTargetDescriptionsResourceId() { 630 return mTargetDescriptionsResourceId; 631 } 632 633 /** 634 * Sets the resource id specifying the target direction descriptions for accessibility. 635 * 636 * @param resourceId The resource id. 637 */ 638 public void setDirectionDescriptionsResourceId(int resourceId) { 639 mDirectionDescriptionsResourceId = resourceId; 640 if (mDirectionDescriptions != null) { 641 mDirectionDescriptions.clear(); 642 } 643 } 644 645 /** 646 * Gets the resource id specifying the target direction descriptions. 647 * 648 * @return The resource id. 649 */ 650 public int getDirectionDescriptionsResourceId() { 651 return mDirectionDescriptionsResourceId; 652 } 653 654 /** 655 * Enable or disable vibrate on touch. 656 * 657 * @param enabled 658 */ 659 public void setVibrateEnabled(boolean enabled) { 660 if (enabled && mVibrator == null) { 661 mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); 662 } else { 663 mVibrator = null; 664 } 665 } 666 667 /** 668 * Starts wave animation. 669 * 670 */ 671 public void ping() { 672 if (mFeedbackCount > 0) { 673 boolean doWaveAnimation = true; 674 final AnimationBundle waveAnimations = mWaveAnimations; 675 676 // Don't do a wave if there's already one in progress 677 if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { 678 long t = waveAnimations.get(0).animator.getCurrentPlayTime(); 679 if (t < WAVE_ANIMATION_DURATION/2) { 680 doWaveAnimation = false; 681 } 682 } 683 684 if (doWaveAnimation) { 685 startWaveAnimation(); 686 } 687 } 688 } 689 690 private void stopAndHideWaveAnimation() { 691 mWaveAnimations.cancel(); 692 mPointCloud.waveManager.setAlpha(0.0f); 693 } 694 695 private void startWaveAnimation() { 696 mWaveAnimations.cancel(); 697 mPointCloud.waveManager.setAlpha(1.0f); 698 mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); 699 mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, 700 "ease", Ease.Quad.easeOut, 701 "delay", 0, 702 "radius", 2.0f * mOuterRadius, 703 "onUpdate", mUpdateListener, 704 "onComplete", 705 new AnimatorListenerAdapter() { 706 public void onAnimationEnd(Animator animator) { 707 mPointCloud.waveManager.setRadius(0.0f); 708 mPointCloud.waveManager.setAlpha(0.0f); 709 } 710 })); 711 mWaveAnimations.start(); 712 } 713 714 /** 715 * Resets the widget to default state and cancels all animation. If animate is 'true', will 716 * animate objects into place. Otherwise, objects will snap back to place. 717 * 718 * @param animate 719 */ 720 public void reset(boolean animate) { 721 mGlowAnimations.stop(); 722 mTargetAnimations.stop(); 723 startBackgroundAnimation(0, 0.0f); 724 stopAndHideWaveAnimation(); 725 hideTargets(animate, false); 726 hideGlow(0, 0, 1.0f, null); 727 Tweener.reset(); 728 } 729 730 private void startBackgroundAnimation(int duration, float alpha) { 731 final Drawable background = getBackground(); 732 if (mAlwaysTrackFinger && background != null) { 733 if (mBackgroundAnimator != null) { 734 mBackgroundAnimator.animator.cancel(); 735 } 736 mBackgroundAnimator = Tweener.to(background, duration, 737 "ease", Ease.Cubic.easeIn, 738 "alpha", (int)(255.0f * alpha), 739 "delay", SHOW_ANIMATION_DELAY); 740 mBackgroundAnimator.animator.start(); 741 } 742 } 743 744 @Override 745 public boolean onTouchEvent(MotionEvent event) { 746 final int action = event.getAction(); 747 boolean handled = false; 748 switch (action) { 749 case MotionEvent.ACTION_DOWN: 750 if (DEBUG) Log.v(TAG, "*** DOWN ***"); 751 handleDown(event); 752 handleMove(event); 753 handled = true; 754 break; 755 756 case MotionEvent.ACTION_MOVE: 757 if (DEBUG) Log.v(TAG, "*** MOVE ***"); 758 handleMove(event); 759 handled = true; 760 break; 761 762 case MotionEvent.ACTION_UP: 763 if (DEBUG) Log.v(TAG, "*** UP ***"); 764 handleMove(event); 765 handleUp(event); 766 handled = true; 767 break; 768 769 case MotionEvent.ACTION_CANCEL: 770 if (DEBUG) Log.v(TAG, "*** CANCEL ***"); 771 handleMove(event); 772 handleCancel(event); 773 handled = true; 774 break; 775 } 776 invalidate(); 777 return handled ? true : super.onTouchEvent(event); 778 } 779 780 private void updateGlowPosition(float x, float y) { 781 mPointCloud.glowManager.setX(x); 782 mPointCloud.glowManager.setY(y); 783 } 784 785 private void handleDown(MotionEvent event) { 786 float eventX = event.getX(); 787 float eventY = event.getY(); 788 switchToState(STATE_START, eventX, eventY); 789 if (!trySwitchToFirstTouchState(eventX, eventY)) { 790 mDragging = false; 791 } else { 792 updateGlowPosition(eventX, eventY); 793 } 794 } 795 796 private void handleUp(MotionEvent event) { 797 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); 798 switchToState(STATE_FINISH, event.getX(), event.getY()); 799 } 800 801 private void handleCancel(MotionEvent event) { 802 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); 803 804 // We should drop the active target here but it interferes with 805 // moving off the screen in the direction of the navigation bar. At some point we may 806 // want to revisit how we handle this. For now we'll allow a canceled event to 807 // activate the current target. 808 809 // mActiveTarget = -1; // Drop the active target if canceled. 810 811 switchToState(STATE_FINISH, event.getX(), event.getY()); 812 } 813 814 private void handleMove(MotionEvent event) { 815 int activeTarget = -1; 816 final int historySize = event.getHistorySize(); 817 ArrayList<TargetDrawable> targets = mTargetDrawables; 818 int ntargets = targets.size(); 819 float x = 0.0f; 820 float y = 0.0f; 821 for (int k = 0; k < historySize + 1; k++) { 822 float eventX = k < historySize ? event.getHistoricalX(k) : event.getX(); 823 float eventY = k < historySize ? event.getHistoricalY(k) : event.getY(); 824 // tx and ty are relative to wave center 825 float tx = eventX - mWaveCenterX; 826 float ty = eventY - mWaveCenterY; 827 float touchRadius = (float) Math.sqrt(dist2(tx, ty)); 828 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; 829 float limitX = tx * scale; 830 float limitY = ty * scale; 831 double angleRad = Math.atan2(-ty, tx); 832 833 if (!mDragging) { 834 trySwitchToFirstTouchState(eventX, eventY); 835 } 836 837 if (mDragging) { 838 // For multiple targets, snap to the one that matches 839 final float snapRadius = mOuterRadius - mSnapMargin; 840 final float snapDistance2 = snapRadius * snapRadius; 841 // Find first target in range 842 for (int i = 0; i < ntargets; i++) { 843 TargetDrawable target = targets.get(i); 844 845 double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; 846 double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; 847 if (target.isEnabled()) { 848 boolean angleMatches = 849 (angleRad > targetMinRad && angleRad <= targetMaxRad) || 850 (angleRad + 2 * Math.PI > targetMinRad && 851 angleRad + 2 * Math.PI <= targetMaxRad); 852 if (angleMatches && (dist2(tx, ty) > snapDistance2)) { 853 activeTarget = i; 854 } 855 } 856 } 857 } 858 x = limitX; 859 y = limitY; 860 } 861 862 if (!mDragging) { 863 return; 864 } 865 866 if (activeTarget != -1) { 867 switchToState(STATE_SNAP, x,y); 868 updateGlowPosition(x, y); 869 } else { 870 switchToState(STATE_TRACKING, x, y); 871 updateGlowPosition(x, y); 872 } 873 874 if (mActiveTarget != activeTarget) { 875 // Defocus the old target 876 if (mActiveTarget != -1) { 877 TargetDrawable target = targets.get(mActiveTarget); 878 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 879 target.setState(TargetDrawable.STATE_INACTIVE); 880 } 881 } 882 // Focus the new target 883 if (activeTarget != -1) { 884 TargetDrawable target = targets.get(activeTarget); 885 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 886 target.setState(TargetDrawable.STATE_FOCUSED); 887 } 888 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 889 String targetContentDescription = getTargetDescription(activeTarget); 890 announceText(targetContentDescription); 891 } 892 } 893 } 894 mActiveTarget = activeTarget; 895 } 896 897 @Override 898 public boolean onHoverEvent(MotionEvent event) { 899 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 900 final int action = event.getAction(); 901 switch (action) { 902 case MotionEvent.ACTION_HOVER_ENTER: 903 event.setAction(MotionEvent.ACTION_DOWN); 904 break; 905 case MotionEvent.ACTION_HOVER_MOVE: 906 event.setAction(MotionEvent.ACTION_MOVE); 907 break; 908 case MotionEvent.ACTION_HOVER_EXIT: 909 event.setAction(MotionEvent.ACTION_UP); 910 break; 911 } 912 onTouchEvent(event); 913 event.setAction(action); 914 } 915 return super.onHoverEvent(event); 916 } 917 918 /** 919 * Sets the current grabbed state, and dispatches a grabbed state change 920 * event to our listener. 921 */ 922 private void setGrabbedState(int newState) { 923 if (newState != mGrabbedState) { 924 if (newState != OnTriggerListener.NO_HANDLE) { 925 vibrate(); 926 } 927 mGrabbedState = newState; 928 if (mOnTriggerListener != null) { 929 if (newState == OnTriggerListener.NO_HANDLE) { 930 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); 931 } else { 932 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); 933 } 934 mOnTriggerListener.onGrabbedStateChange(this, newState); 935 } 936 } 937 } 938 939 private boolean trySwitchToFirstTouchState(float x, float y) { 940 final float tx = x - mWaveCenterX; 941 final float ty = y - mWaveCenterY; 942 if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { 943 if (DEBUG) Log.v(TAG, "** Handle HIT"); 944 switchToState(STATE_FIRST_TOUCH, x, y); 945 updateGlowPosition(tx, ty); 946 mDragging = true; 947 return true; 948 } 949 return false; 950 } 951 952 private void assignDefaultsIfNeeded() { 953 if (mOuterRadius == 0.0f) { 954 mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; 955 } 956 if (mSnapMargin == 0.0f) { 957 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 958 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); 959 } 960 if (mInnerRadius == 0.0f) { 961 mInnerRadius = mHandleDrawable.getWidth() / 10.0f; 962 } 963 } 964 965 private void computeInsets(int dx, int dy) { 966 final int layoutDirection = getResolvedLayoutDirection(); 967 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 968 969 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 970 case Gravity.LEFT: 971 mHorizontalInset = 0; 972 break; 973 case Gravity.RIGHT: 974 mHorizontalInset = dx; 975 break; 976 case Gravity.CENTER_HORIZONTAL: 977 default: 978 mHorizontalInset = dx / 2; 979 break; 980 } 981 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 982 case Gravity.TOP: 983 mVerticalInset = 0; 984 break; 985 case Gravity.BOTTOM: 986 mVerticalInset = dy; 987 break; 988 case Gravity.CENTER_VERTICAL: 989 default: 990 mVerticalInset = dy / 2; 991 break; 992 } 993 } 994 995 @Override 996 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 997 super.onLayout(changed, left, top, right, bottom); 998 final int width = right - left; 999 final int height = bottom - top; 1000 1001 // Target placement width/height. This puts the targets on the greater of the ring 1002 // width or the specified outer radius. 1003 final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); 1004 final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); 1005 float newWaveCenterX = mHorizontalInset 1006 + Math.max(width, mMaxTargetWidth + placementWidth) / 2; 1007 float newWaveCenterY = mVerticalInset 1008 + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; 1009 1010 if (mInitialLayout) { 1011 stopAndHideWaveAnimation(); 1012 hideTargets(false, false); 1013 mInitialLayout = false; 1014 } 1015 1016 mOuterRing.setPositionX(newWaveCenterX); 1017 mOuterRing.setPositionY(newWaveCenterY); 1018 1019 mHandleDrawable.setPositionX(newWaveCenterX); 1020 mHandleDrawable.setPositionY(newWaveCenterY); 1021 1022 updateTargetPositions(newWaveCenterX, newWaveCenterY); 1023 updatePointCloudPosition(newWaveCenterX, newWaveCenterY); 1024 updateGlowPosition(newWaveCenterX, newWaveCenterY); 1025 1026 mWaveCenterX = newWaveCenterX; 1027 mWaveCenterY = newWaveCenterY; 1028 1029 if (DEBUG) dump(); 1030 } 1031 1032 private void updateTargetPositions(float centerX, float centerY) { 1033 // Reposition the target drawables if the view changed. 1034 ArrayList<TargetDrawable> targets = mTargetDrawables; 1035 final int size = targets.size(); 1036 final float alpha = (float) (-2.0f * Math.PI / size); 1037 for (int i = 0; i < size; i++) { 1038 final TargetDrawable targetIcon = targets.get(i); 1039 final float angle = alpha * i; 1040 targetIcon.setPositionX(centerX); 1041 targetIcon.setPositionY(centerY); 1042 targetIcon.setX(mOuterRadius * (float) Math.cos(angle)); 1043 targetIcon.setY(mOuterRadius * (float) Math.sin(angle)); 1044 } 1045 } 1046 1047 private void updatePointCloudPosition(float centerX, float centerY) { 1048 mPointCloud.setCenter(centerX, centerY); 1049 } 1050 1051 @Override 1052 protected void onDraw(Canvas canvas) { 1053 mPointCloud.draw(canvas); 1054 mOuterRing.draw(canvas); 1055 final int ntargets = mTargetDrawables.size(); 1056 for (int i = 0; i < ntargets; i++) { 1057 TargetDrawable target = mTargetDrawables.get(i); 1058 if (target != null) { 1059 target.draw(canvas); 1060 } 1061 } 1062 mHandleDrawable.draw(canvas); 1063 } 1064 1065 public void setOnTriggerListener(OnTriggerListener listener) { 1066 mOnTriggerListener = listener; 1067 } 1068 1069 private float square(float d) { 1070 return d * d; 1071 } 1072 1073 private float dist2(float dx, float dy) { 1074 return dx*dx + dy*dy; 1075 } 1076 1077 private float getScaledGlowRadiusSquared() { 1078 final float scaledTapRadius; 1079 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 1080 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; 1081 } else { 1082 scaledTapRadius = mGlowRadius; 1083 } 1084 return square(scaledTapRadius); 1085 } 1086 1087 private void announceTargets() { 1088 StringBuilder utterance = new StringBuilder(); 1089 final int targetCount = mTargetDrawables.size(); 1090 for (int i = 0; i < targetCount; i++) { 1091 String targetDescription = getTargetDescription(i); 1092 String directionDescription = getDirectionDescription(i); 1093 if (!TextUtils.isEmpty(targetDescription) 1094 && !TextUtils.isEmpty(directionDescription)) { 1095 String text = String.format(directionDescription, targetDescription); 1096 utterance.append(text); 1097 } 1098 if (utterance.length() > 0) { 1099 announceText(utterance.toString()); 1100 } 1101 } 1102 } 1103 1104 private void announceText(String text) { 1105 setContentDescription(text); 1106 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1107 setContentDescription(null); 1108 } 1109 1110 private String getTargetDescription(int index) { 1111 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { 1112 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); 1113 if (mTargetDrawables.size() != mTargetDescriptions.size()) { 1114 Log.w(TAG, "The number of target drawables must be" 1115 + " equal to the number of target descriptions."); 1116 return null; 1117 } 1118 } 1119 return mTargetDescriptions.get(index); 1120 } 1121 1122 private String getDirectionDescription(int index) { 1123 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { 1124 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); 1125 if (mTargetDrawables.size() != mDirectionDescriptions.size()) { 1126 Log.w(TAG, "The number of target drawables must be" 1127 + " equal to the number of direction descriptions."); 1128 return null; 1129 } 1130 } 1131 return mDirectionDescriptions.get(index); 1132 } 1133 1134 private ArrayList<String> loadDescriptions(int resourceId) { 1135 TypedArray array = getContext().getResources().obtainTypedArray(resourceId); 1136 final int count = array.length(); 1137 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); 1138 for (int i = 0; i < count; i++) { 1139 String contentDescription = array.getString(i); 1140 targetContentDescriptions.add(contentDescription); 1141 } 1142 array.recycle(); 1143 return targetContentDescriptions; 1144 } 1145 1146 public int getResourceIdForTarget(int index) { 1147 final TargetDrawable drawable = mTargetDrawables.get(index); 1148 return drawable == null ? 0 : drawable.getResourceId(); 1149 } 1150 1151 public void setEnableTarget(int resourceId, boolean enabled) { 1152 for (int i = 0; i < mTargetDrawables.size(); i++) { 1153 final TargetDrawable target = mTargetDrawables.get(i); 1154 if (target.getResourceId() == resourceId) { 1155 target.setEnabled(enabled); 1156 break; // should never be more than one match 1157 } 1158 } 1159 } 1160 1161 /** 1162 * Gets the position of a target in the array that matches the given resource. 1163 * @param resourceId 1164 * @return the index or -1 if not found 1165 */ 1166 public int getTargetPosition(int resourceId) { 1167 for (int i = 0; i < mTargetDrawables.size(); i++) { 1168 final TargetDrawable target = mTargetDrawables.get(i); 1169 if (target.getResourceId() == resourceId) { 1170 return i; // should never be more than one match 1171 } 1172 } 1173 return -1; 1174 } 1175 1176 private boolean replaceTargetDrawables(Resources res, int existingResourceId, 1177 int newResourceId) { 1178 if (existingResourceId == 0 || newResourceId == 0) { 1179 return false; 1180 } 1181 1182 boolean result = false; 1183 final ArrayList<TargetDrawable> drawables = mTargetDrawables; 1184 final int size = drawables.size(); 1185 for (int i = 0; i < size; i++) { 1186 final TargetDrawable target = drawables.get(i); 1187 if (target != null && target.getResourceId() == existingResourceId) { 1188 target.setDrawable(res, newResourceId); 1189 result = true; 1190 } 1191 } 1192 1193 if (result) { 1194 requestLayout(); // in case any given drawable's size changes 1195 } 1196 1197 return result; 1198 } 1199 1200 /** 1201 * Searches the given package for a resource to use to replace the Drawable on the 1202 * target with the given resource id 1203 * @param component of the .apk that contains the resource 1204 * @param name of the metadata in the .apk 1205 * @param existingResId the resource id of the target to search for 1206 * @return true if found in the given package and replaced at least one target Drawables 1207 */ 1208 public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, 1209 int existingResId) { 1210 if (existingResId == 0) return false; 1211 1212 try { 1213 PackageManager packageManager = mContext.getPackageManager(); 1214 // Look for the search icon specified in the activity meta-data 1215 Bundle metaData = packageManager.getActivityInfo( 1216 component, PackageManager.GET_META_DATA).metaData; 1217 if (metaData != null) { 1218 int iconResId = metaData.getInt(name); 1219 if (iconResId != 0) { 1220 Resources res = packageManager.getResourcesForActivity(component); 1221 return replaceTargetDrawables(res, existingResId, iconResId); 1222 } 1223 } 1224 } catch (NameNotFoundException e) { 1225 Log.w(TAG, "Failed to swap drawable; " 1226 + component.flattenToShortString() + " not found", e); 1227 } catch (Resources.NotFoundException nfe) { 1228 Log.w(TAG, "Failed to swap drawable from " 1229 + component.flattenToShortString(), nfe); 1230 } 1231 return false; 1232 } 1233} 1234