GlowPadView.java revision 245b453733d0b611960844d939e0013f285a5a9a
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.AccessibilityManager; 43 44import com.android.internal.R; 45 46import java.util.ArrayList; 47 48/** 49 * A re-usable widget containing a center, outer ring and wave animation. 50 */ 51public class GlowPadView extends View { 52 private static final String TAG = "GlowPadView"; 53 private static final boolean DEBUG = false; 54 55 // Wave state machine 56 private static final int STATE_IDLE = 0; 57 private static final int STATE_START = 1; 58 private static final int STATE_FIRST_TOUCH = 2; 59 private static final int STATE_TRACKING = 3; 60 private static final int STATE_SNAP = 4; 61 private static final int STATE_FINISH = 5; 62 63 // Animation properties. 64 private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it 65 66 public interface OnTriggerListener { 67 int NO_HANDLE = 0; 68 int CENTER_HANDLE = 1; 69 public void onGrabbed(View v, int handle); 70 public void onReleased(View v, int handle); 71 public void onTrigger(View v, int target); 72 public void onGrabbedStateChange(View v, int handle); 73 public void onFinishFinalAnimation(); 74 } 75 76 // Tuneable parameters for animation 77 private static final int WAVE_ANIMATION_DURATION = 1350; 78 private static final int RETURN_TO_HOME_DELAY = 1200; 79 private static final int RETURN_TO_HOME_DURATION = 200; 80 private static final int HIDE_ANIMATION_DELAY = 200; 81 private static final int HIDE_ANIMATION_DURATION = 200; 82 private static final int SHOW_ANIMATION_DURATION = 200; 83 private static final int SHOW_ANIMATION_DELAY = 50; 84 private static final int INITIAL_SHOW_HANDLE_DURATION = 200; 85 private static final int REVEAL_GLOW_DELAY = 0; 86 private static final int REVEAL_GLOW_DURATION = 0; 87 88 private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; 89 private static final float TARGET_SCALE_EXPANDED = 1.0f; 90 private static final float TARGET_SCALE_COLLAPSED = 0.8f; 91 private static final float RING_SCALE_EXPANDED = 1.0f; 92 private static final float RING_SCALE_COLLAPSED = 0.5f; 93 94 private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); 95 private AnimationBundle mWaveAnimations = new AnimationBundle(); 96 private AnimationBundle mTargetAnimations = new AnimationBundle(); 97 private AnimationBundle mGlowAnimations = new AnimationBundle(); 98 private ArrayList<String> mTargetDescriptions; 99 private ArrayList<String> mDirectionDescriptions; 100 private OnTriggerListener mOnTriggerListener; 101 private TargetDrawable mHandleDrawable; 102 private TargetDrawable mOuterRing; 103 private Vibrator mVibrator; 104 105 private int mFeedbackCount = 3; 106 private int mVibrationDuration = 0; 107 private int mGrabbedState; 108 private int mActiveTarget = -1; 109 private float mGlowRadius; 110 private float mWaveCenterX; 111 private float mWaveCenterY; 112 private int mMaxTargetHeight; 113 private int mMaxTargetWidth; 114 115 private float mOuterRadius = 0.0f; 116 private float mSnapMargin = 0.0f; 117 private boolean mDragging; 118 private int mNewTargetResources; 119 120 private class AnimationBundle extends ArrayList<Tweener> { 121 private static final long serialVersionUID = 0xA84D78726F127468L; 122 private boolean mSuspended; 123 124 public void start() { 125 if (mSuspended) return; // ignore attempts to start animations 126 final int count = size(); 127 for (int i = 0; i < count; i++) { 128 Tweener anim = get(i); 129 anim.animator.start(); 130 } 131 } 132 133 public void cancel() { 134 final int count = size(); 135 for (int i = 0; i < count; i++) { 136 Tweener anim = get(i); 137 anim.animator.cancel(); 138 } 139 clear(); 140 } 141 142 public void stop() { 143 final int count = size(); 144 for (int i = 0; i < count; i++) { 145 Tweener anim = get(i); 146 anim.animator.end(); 147 } 148 clear(); 149 } 150 151 public void setSuspended(boolean suspend) { 152 mSuspended = suspend; 153 } 154 }; 155 156 private AnimatorListener mResetListener = new AnimatorListenerAdapter() { 157 public void onAnimationEnd(Animator animator) { 158 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 159 dispatchOnFinishFinalAnimation(); 160 } 161 }; 162 163 private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { 164 public void onAnimationEnd(Animator animator) { 165 ping(); 166 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 167 dispatchOnFinishFinalAnimation(); 168 } 169 }; 170 171 private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { 172 public void onAnimationUpdate(ValueAnimator animation) { 173 invalidate(); 174 } 175 }; 176 177 private boolean mAnimatingTargets; 178 private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { 179 public void onAnimationEnd(Animator animator) { 180 if (mNewTargetResources != 0) { 181 internalSetTargetResources(mNewTargetResources); 182 mNewTargetResources = 0; 183 hideTargets(false, false); 184 } 185 mAnimatingTargets = false; 186 } 187 }; 188 private int mTargetResourceId; 189 private int mTargetDescriptionsResourceId; 190 private int mDirectionDescriptionsResourceId; 191 private boolean mAlwaysTrackFinger; 192 private int mHorizontalInset; 193 private int mVerticalInset; 194 private int mGravity = Gravity.TOP; 195 private boolean mInitialLayout = true; 196 private Tweener mBackgroundAnimator; 197 private PointCloud mPointCloud; 198 private float mInnerRadius; 199 private int mPointerId; 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 TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); 218 mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0); 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, 0.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.getActionMasked(); 741 boolean handled = false; 742 switch (action) { 743 case MotionEvent.ACTION_POINTER_DOWN: 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_POINTER_UP: 758 case MotionEvent.ACTION_UP: 759 if (DEBUG) Log.v(TAG, "*** UP ***"); 760 handleMove(event); 761 handleUp(event); 762 handled = true; 763 break; 764 765 case MotionEvent.ACTION_CANCEL: 766 if (DEBUG) Log.v(TAG, "*** CANCEL ***"); 767 handleMove(event); 768 handleCancel(event); 769 handled = true; 770 break; 771 772 } 773 invalidate(); 774 return handled ? true : super.onTouchEvent(event); 775 } 776 777 private void updateGlowPosition(float x, float y) { 778 mPointCloud.glowManager.setX(x); 779 mPointCloud.glowManager.setY(y); 780 } 781 782 private void handleDown(MotionEvent event) { 783 int actionIndex = event.getActionIndex(); 784 float eventX = event.getX(actionIndex); 785 float eventY = event.getY(actionIndex); 786 switchToState(STATE_START, eventX, eventY); 787 if (!trySwitchToFirstTouchState(eventX, eventY)) { 788 mDragging = false; 789 } else { 790 mPointerId = event.getPointerId(actionIndex); 791 updateGlowPosition(eventX, eventY); 792 } 793 } 794 795 private void handleUp(MotionEvent event) { 796 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); 797 int actionIndex = event.getActionIndex(); 798 if (event.getPointerId(actionIndex) == mPointerId) { 799 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); 800 } 801 } 802 803 private void handleCancel(MotionEvent event) { 804 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); 805 806 // Drop the active target if canceled. 807 mActiveTarget = -1; 808 809 int actionIndex = event.findPointerIndex(mPointerId); 810 actionIndex = actionIndex == -1 ? 0 : actionIndex; 811 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); 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 int actionIndex = event.findPointerIndex(mPointerId); 822 823 if (actionIndex == -1) { 824 return; // no data for this pointer 825 } 826 827 for (int k = 0; k < historySize + 1; k++) { 828 float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) 829 : event.getX(actionIndex); 830 float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) 831 : event.getY(actionIndex); 832 // tx and ty are relative to wave center 833 float tx = eventX - mWaveCenterX; 834 float ty = eventY - mWaveCenterY; 835 float touchRadius = (float) Math.sqrt(dist2(tx, ty)); 836 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; 837 float limitX = tx * scale; 838 float limitY = ty * scale; 839 double angleRad = Math.atan2(-ty, tx); 840 841 if (!mDragging) { 842 trySwitchToFirstTouchState(eventX, eventY); 843 } 844 845 if (mDragging) { 846 // For multiple targets, snap to the one that matches 847 final float snapRadius = mOuterRadius - mSnapMargin; 848 final float snapDistance2 = snapRadius * snapRadius; 849 // Find first target in range 850 for (int i = 0; i < ntargets; i++) { 851 TargetDrawable target = targets.get(i); 852 853 double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; 854 double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; 855 if (target.isEnabled()) { 856 boolean angleMatches = 857 (angleRad > targetMinRad && angleRad <= targetMaxRad) || 858 (angleRad + 2 * Math.PI > targetMinRad && 859 angleRad + 2 * Math.PI <= targetMaxRad); 860 if (angleMatches && (dist2(tx, ty) > snapDistance2)) { 861 activeTarget = i; 862 } 863 } 864 } 865 } 866 x = limitX; 867 y = limitY; 868 } 869 870 if (!mDragging) { 871 return; 872 } 873 874 if (activeTarget != -1) { 875 switchToState(STATE_SNAP, x,y); 876 updateGlowPosition(x, y); 877 } else { 878 switchToState(STATE_TRACKING, x, y); 879 updateGlowPosition(x, y); 880 } 881 882 if (mActiveTarget != activeTarget) { 883 // Defocus the old target 884 if (mActiveTarget != -1) { 885 TargetDrawable target = targets.get(mActiveTarget); 886 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 887 target.setState(TargetDrawable.STATE_INACTIVE); 888 } 889 } 890 // Focus the new target 891 if (activeTarget != -1) { 892 TargetDrawable target = targets.get(activeTarget); 893 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 894 target.setState(TargetDrawable.STATE_FOCUSED); 895 } 896 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 897 String targetContentDescription = getTargetDescription(activeTarget); 898 announceForAccessibility(targetContentDescription); 899 } 900 } 901 } 902 mActiveTarget = activeTarget; 903 } 904 905 @Override 906 public boolean onHoverEvent(MotionEvent event) { 907 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 908 final int action = event.getAction(); 909 switch (action) { 910 case MotionEvent.ACTION_HOVER_ENTER: 911 event.setAction(MotionEvent.ACTION_DOWN); 912 break; 913 case MotionEvent.ACTION_HOVER_MOVE: 914 event.setAction(MotionEvent.ACTION_MOVE); 915 break; 916 case MotionEvent.ACTION_HOVER_EXIT: 917 event.setAction(MotionEvent.ACTION_UP); 918 break; 919 } 920 onTouchEvent(event); 921 event.setAction(action); 922 } 923 return super.onHoverEvent(event); 924 } 925 926 /** 927 * Sets the current grabbed state, and dispatches a grabbed state change 928 * event to our listener. 929 */ 930 private void setGrabbedState(int newState) { 931 if (newState != mGrabbedState) { 932 if (newState != OnTriggerListener.NO_HANDLE) { 933 vibrate(); 934 } 935 mGrabbedState = newState; 936 if (mOnTriggerListener != null) { 937 if (newState == OnTriggerListener.NO_HANDLE) { 938 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); 939 } else { 940 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); 941 } 942 mOnTriggerListener.onGrabbedStateChange(this, newState); 943 } 944 } 945 } 946 947 private boolean trySwitchToFirstTouchState(float x, float y) { 948 final float tx = x - mWaveCenterX; 949 final float ty = y - mWaveCenterY; 950 if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { 951 if (DEBUG) Log.v(TAG, "** Handle HIT"); 952 switchToState(STATE_FIRST_TOUCH, x, y); 953 updateGlowPosition(tx, ty); 954 mDragging = true; 955 return true; 956 } 957 return false; 958 } 959 960 private void assignDefaultsIfNeeded() { 961 if (mOuterRadius == 0.0f) { 962 mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; 963 } 964 if (mSnapMargin == 0.0f) { 965 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 966 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); 967 } 968 if (mInnerRadius == 0.0f) { 969 mInnerRadius = mHandleDrawable.getWidth() / 10.0f; 970 } 971 } 972 973 private void computeInsets(int dx, int dy) { 974 final int layoutDirection = getLayoutDirection(); 975 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 976 977 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 978 case Gravity.LEFT: 979 mHorizontalInset = 0; 980 break; 981 case Gravity.RIGHT: 982 mHorizontalInset = dx; 983 break; 984 case Gravity.CENTER_HORIZONTAL: 985 default: 986 mHorizontalInset = dx / 2; 987 break; 988 } 989 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 990 case Gravity.TOP: 991 mVerticalInset = 0; 992 break; 993 case Gravity.BOTTOM: 994 mVerticalInset = dy; 995 break; 996 case Gravity.CENTER_VERTICAL: 997 default: 998 mVerticalInset = dy / 2; 999 break; 1000 } 1001 } 1002 1003 @Override 1004 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1005 super.onLayout(changed, left, top, right, bottom); 1006 final int width = right - left; 1007 final int height = bottom - top; 1008 1009 // Target placement width/height. This puts the targets on the greater of the ring 1010 // width or the specified outer radius. 1011 final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); 1012 final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); 1013 float newWaveCenterX = mHorizontalInset 1014 + Math.max(width, mMaxTargetWidth + placementWidth) / 2; 1015 float newWaveCenterY = mVerticalInset 1016 + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; 1017 1018 if (mInitialLayout) { 1019 stopAndHideWaveAnimation(); 1020 hideTargets(false, false); 1021 mInitialLayout = false; 1022 } 1023 1024 mOuterRing.setPositionX(newWaveCenterX); 1025 mOuterRing.setPositionY(newWaveCenterY); 1026 1027 mHandleDrawable.setPositionX(newWaveCenterX); 1028 mHandleDrawable.setPositionY(newWaveCenterY); 1029 1030 updateTargetPositions(newWaveCenterX, newWaveCenterY); 1031 updatePointCloudPosition(newWaveCenterX, newWaveCenterY); 1032 updateGlowPosition(newWaveCenterX, newWaveCenterY); 1033 1034 mWaveCenterX = newWaveCenterX; 1035 mWaveCenterY = newWaveCenterY; 1036 1037 if (DEBUG) dump(); 1038 } 1039 1040 private void updateTargetPositions(float centerX, float centerY) { 1041 // Reposition the target drawables if the view changed. 1042 ArrayList<TargetDrawable> targets = mTargetDrawables; 1043 final int size = targets.size(); 1044 final float alpha = (float) (-2.0f * Math.PI / size); 1045 for (int i = 0; i < size; i++) { 1046 final TargetDrawable targetIcon = targets.get(i); 1047 final float angle = alpha * i; 1048 targetIcon.setPositionX(centerX); 1049 targetIcon.setPositionY(centerY); 1050 targetIcon.setX(mOuterRadius * (float) Math.cos(angle)); 1051 targetIcon.setY(mOuterRadius * (float) Math.sin(angle)); 1052 } 1053 } 1054 1055 private void updatePointCloudPosition(float centerX, float centerY) { 1056 mPointCloud.setCenter(centerX, centerY); 1057 } 1058 1059 @Override 1060 protected void onDraw(Canvas canvas) { 1061 mPointCloud.draw(canvas); 1062 mOuterRing.draw(canvas); 1063 final int ntargets = mTargetDrawables.size(); 1064 for (int i = 0; i < ntargets; i++) { 1065 TargetDrawable target = mTargetDrawables.get(i); 1066 if (target != null) { 1067 target.draw(canvas); 1068 } 1069 } 1070 mHandleDrawable.draw(canvas); 1071 } 1072 1073 public void setOnTriggerListener(OnTriggerListener listener) { 1074 mOnTriggerListener = listener; 1075 } 1076 1077 private float square(float d) { 1078 return d * d; 1079 } 1080 1081 private float dist2(float dx, float dy) { 1082 return dx*dx + dy*dy; 1083 } 1084 1085 private float getScaledGlowRadiusSquared() { 1086 final float scaledTapRadius; 1087 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 1088 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; 1089 } else { 1090 scaledTapRadius = mGlowRadius; 1091 } 1092 return square(scaledTapRadius); 1093 } 1094 1095 private void announceTargets() { 1096 StringBuilder utterance = new StringBuilder(); 1097 final int targetCount = mTargetDrawables.size(); 1098 for (int i = 0; i < targetCount; i++) { 1099 String targetDescription = getTargetDescription(i); 1100 String directionDescription = getDirectionDescription(i); 1101 if (!TextUtils.isEmpty(targetDescription) 1102 && !TextUtils.isEmpty(directionDescription)) { 1103 String text = String.format(directionDescription, targetDescription); 1104 utterance.append(text); 1105 } 1106 } 1107 if (utterance.length() > 0) { 1108 announceForAccessibility(utterance.toString()); 1109 } 1110 } 1111 1112 private String getTargetDescription(int index) { 1113 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { 1114 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); 1115 if (mTargetDrawables.size() != mTargetDescriptions.size()) { 1116 Log.w(TAG, "The number of target drawables must be" 1117 + " equal to the number of target descriptions."); 1118 return null; 1119 } 1120 } 1121 return mTargetDescriptions.get(index); 1122 } 1123 1124 private String getDirectionDescription(int index) { 1125 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { 1126 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); 1127 if (mTargetDrawables.size() != mDirectionDescriptions.size()) { 1128 Log.w(TAG, "The number of target drawables must be" 1129 + " equal to the number of direction descriptions."); 1130 return null; 1131 } 1132 } 1133 return mDirectionDescriptions.get(index); 1134 } 1135 1136 private ArrayList<String> loadDescriptions(int resourceId) { 1137 TypedArray array = getContext().getResources().obtainTypedArray(resourceId); 1138 final int count = array.length(); 1139 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); 1140 for (int i = 0; i < count; i++) { 1141 String contentDescription = array.getString(i); 1142 targetContentDescriptions.add(contentDescription); 1143 } 1144 array.recycle(); 1145 return targetContentDescriptions; 1146 } 1147 1148 public int getResourceIdForTarget(int index) { 1149 final TargetDrawable drawable = mTargetDrawables.get(index); 1150 return drawable == null ? 0 : drawable.getResourceId(); 1151 } 1152 1153 public void setEnableTarget(int resourceId, boolean enabled) { 1154 for (int i = 0; i < mTargetDrawables.size(); i++) { 1155 final TargetDrawable target = mTargetDrawables.get(i); 1156 if (target.getResourceId() == resourceId) { 1157 target.setEnabled(enabled); 1158 break; // should never be more than one match 1159 } 1160 } 1161 } 1162 1163 /** 1164 * Gets the position of a target in the array that matches the given resource. 1165 * @param resourceId 1166 * @return the index or -1 if not found 1167 */ 1168 public int getTargetPosition(int resourceId) { 1169 for (int i = 0; i < mTargetDrawables.size(); i++) { 1170 final TargetDrawable target = mTargetDrawables.get(i); 1171 if (target.getResourceId() == resourceId) { 1172 return i; // should never be more than one match 1173 } 1174 } 1175 return -1; 1176 } 1177 1178 private boolean replaceTargetDrawables(Resources res, int existingResourceId, 1179 int newResourceId) { 1180 if (existingResourceId == 0 || newResourceId == 0) { 1181 return false; 1182 } 1183 1184 boolean result = false; 1185 final ArrayList<TargetDrawable> drawables = mTargetDrawables; 1186 final int size = drawables.size(); 1187 for (int i = 0; i < size; i++) { 1188 final TargetDrawable target = drawables.get(i); 1189 if (target != null && target.getResourceId() == existingResourceId) { 1190 target.setDrawable(res, newResourceId); 1191 result = true; 1192 } 1193 } 1194 1195 if (result) { 1196 requestLayout(); // in case any given drawable's size changes 1197 } 1198 1199 return result; 1200 } 1201 1202 /** 1203 * Searches the given package for a resource to use to replace the Drawable on the 1204 * target with the given resource id 1205 * @param component of the .apk that contains the resource 1206 * @param name of the metadata in the .apk 1207 * @param existingResId the resource id of the target to search for 1208 * @return true if found in the given package and replaced at least one target Drawables 1209 */ 1210 public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, 1211 int existingResId) { 1212 if (existingResId == 0) return false; 1213 1214 boolean replaced = false; 1215 if (component != null) { 1216 try { 1217 PackageManager packageManager = mContext.getPackageManager(); 1218 // Look for the search icon specified in the activity meta-data 1219 Bundle metaData = packageManager.getActivityInfo( 1220 component, PackageManager.GET_META_DATA).metaData; 1221 if (metaData != null) { 1222 int iconResId = metaData.getInt(name); 1223 if (iconResId != 0) { 1224 Resources res = packageManager.getResourcesForActivity(component); 1225 replaced = replaceTargetDrawables(res, existingResId, iconResId); 1226 } 1227 } 1228 } catch (NameNotFoundException e) { 1229 Log.w(TAG, "Failed to swap drawable; " 1230 + component.flattenToShortString() + " not found", e); 1231 } catch (Resources.NotFoundException nfe) { 1232 Log.w(TAG, "Failed to swap drawable from " 1233 + component.flattenToShortString(), nfe); 1234 } 1235 } 1236 if (!replaced) { 1237 // Restore the original drawable 1238 replaceTargetDrawables(mContext.getResources(), existingResId, existingResId); 1239 } 1240 return replaced; 1241 } 1242} 1243