GlowPadView.java revision 723a725e790d269f32980116e775d3d7f0037865
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.UserHandle; 35import android.os.Vibrator; 36import android.provider.Settings; 37import android.text.TextUtils; 38import android.util.AttributeSet; 39import android.util.Log; 40import android.util.TypedValue; 41import android.view.Gravity; 42import android.view.MotionEvent; 43import android.view.View; 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 private int mPointerId; 202 203 public GlowPadView(Context context) { 204 this(context, null); 205 } 206 207 public GlowPadView(Context context, AttributeSet attrs) { 208 super(context, attrs); 209 Resources res = context.getResources(); 210 211 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView); 212 mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius); 213 mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius); 214 mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); 215 mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, 216 mVibrationDuration); 217 mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, 218 mFeedbackCount); 219 TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); 220 mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0); 221 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); 222 mOuterRing = new TargetDrawable(res, 223 getResourceId(a, R.styleable.GlowPadView_outerRingDrawable)); 224 225 mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false); 226 227 int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); 228 Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null; 229 mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); 230 231 TypedValue outValue = new TypedValue(); 232 233 // Read array of target drawables 234 if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { 235 internalSetTargetResources(outValue.resourceId); 236 } 237 if (mTargetDrawables == null || mTargetDrawables.size() == 0) { 238 throw new IllegalStateException("Must specify at least one target drawable"); 239 } 240 241 // Read array of target descriptions 242 if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) { 243 final int resourceId = outValue.resourceId; 244 if (resourceId == 0) { 245 throw new IllegalStateException("Must specify target descriptions"); 246 } 247 setTargetDescriptionsResourceId(resourceId); 248 } 249 250 // Read array of direction descriptions 251 if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) { 252 final int resourceId = outValue.resourceId; 253 if (resourceId == 0) { 254 throw new IllegalStateException("Must specify direction descriptions"); 255 } 256 setDirectionDescriptionsResourceId(resourceId); 257 } 258 259 a.recycle(); 260 261 // Use gravity attribute from LinearLayout 262 a = context.obtainStyledAttributes(attrs, android.R.styleable.LinearLayout); 263 mGravity = a.getInt(android.R.styleable.LinearLayout_gravity, Gravity.TOP); 264 a.recycle(); 265 266 setVibrateEnabled(mVibrationDuration > 0); 267 268 assignDefaultsIfNeeded(); 269 270 mPointCloud = new PointCloud(pointDrawable); 271 mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); 272 mPointCloud.glowManager.setRadius(mGlowRadius); 273 } 274 275 private int getResourceId(TypedArray a, int id) { 276 TypedValue tv = a.peekValue(id); 277 return tv == null ? 0 : tv.resourceId; 278 } 279 280 private void dump() { 281 Log.v(TAG, "Outer Radius = " + mOuterRadius); 282 Log.v(TAG, "SnapMargin = " + mSnapMargin); 283 Log.v(TAG, "FeedbackCount = " + mFeedbackCount); 284 Log.v(TAG, "VibrationDuration = " + mVibrationDuration); 285 Log.v(TAG, "GlowRadius = " + mGlowRadius); 286 Log.v(TAG, "WaveCenterX = " + mWaveCenterX); 287 Log.v(TAG, "WaveCenterY = " + mWaveCenterY); 288 } 289 290 public void suspendAnimations() { 291 mWaveAnimations.setSuspended(true); 292 mTargetAnimations.setSuspended(true); 293 mGlowAnimations.setSuspended(true); 294 } 295 296 public void resumeAnimations() { 297 mWaveAnimations.setSuspended(false); 298 mTargetAnimations.setSuspended(false); 299 mGlowAnimations.setSuspended(false); 300 mWaveAnimations.start(); 301 mTargetAnimations.start(); 302 mGlowAnimations.start(); 303 } 304 305 @Override 306 protected int getSuggestedMinimumWidth() { 307 // View should be large enough to contain the background + handle and 308 // target drawable on either edge. 309 return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); 310 } 311 312 @Override 313 protected int getSuggestedMinimumHeight() { 314 // View should be large enough to contain the unlock ring + target and 315 // target drawable on either edge 316 return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); 317 } 318 319 private int resolveMeasured(int measureSpec, int desired) 320 { 321 int result = 0; 322 int specSize = MeasureSpec.getSize(measureSpec); 323 switch (MeasureSpec.getMode(measureSpec)) { 324 case MeasureSpec.UNSPECIFIED: 325 result = desired; 326 break; 327 case MeasureSpec.AT_MOST: 328 result = Math.min(specSize, desired); 329 break; 330 case MeasureSpec.EXACTLY: 331 default: 332 result = specSize; 333 } 334 return result; 335 } 336 337 @Override 338 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 339 final int minimumWidth = getSuggestedMinimumWidth(); 340 final int minimumHeight = getSuggestedMinimumHeight(); 341 int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 342 int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 343 computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight)); 344 setMeasuredDimension(computedWidth, computedHeight); 345 } 346 347 private void switchToState(int state, float x, float y) { 348 switch (state) { 349 case STATE_IDLE: 350 deactivateTargets(); 351 hideGlow(0, 0, 0.0f, null); 352 startBackgroundAnimation(0, 0.0f); 353 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); 354 mHandleDrawable.setAlpha(1.0f); 355 break; 356 357 case STATE_START: 358 startBackgroundAnimation(0, 0.0f); 359 break; 360 361 case STATE_FIRST_TOUCH: 362 mHandleDrawable.setAlpha(0.0f); 363 deactivateTargets(); 364 showTargets(true); 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 final float targetScale = expanded ? 489 TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; 490 final int length = mTargetDrawables.size(); 491 final TimeInterpolator interpolator = Ease.Cubic.easeOut; 492 for (int i = 0; i < length; i++) { 493 TargetDrawable target = mTargetDrawables.get(i); 494 target.setState(TargetDrawable.STATE_INACTIVE); 495 mTargetAnimations.add(Tweener.to(target, duration, 496 "ease", interpolator, 497 "alpha", 0.0f, 498 "scaleX", targetScale, 499 "scaleY", targetScale, 500 "delay", delay, 501 "onUpdate", mUpdateListener)); 502 } 503 504 final float ringScaleTarget = expanded ? 505 RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; 506 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 507 "ease", interpolator, 508 "alpha", 0.0f, 509 "scaleX", ringScaleTarget, 510 "scaleY", ringScaleTarget, 511 "delay", delay, 512 "onUpdate", mUpdateListener, 513 "onComplete", mTargetUpdateListener)); 514 515 mTargetAnimations.start(); 516 } 517 518 private void showTargets(boolean animate) { 519 mTargetAnimations.stop(); 520 mAnimatingTargets = animate; 521 final int delay = animate ? SHOW_ANIMATION_DELAY : 0; 522 final int duration = animate ? SHOW_ANIMATION_DURATION : 0; 523 final int length = mTargetDrawables.size(); 524 for (int i = 0; i < length; i++) { 525 TargetDrawable target = mTargetDrawables.get(i); 526 target.setState(TargetDrawable.STATE_INACTIVE); 527 mTargetAnimations.add(Tweener.to(target, duration, 528 "ease", Ease.Cubic.easeOut, 529 "alpha", 1.0f, 530 "scaleX", 1.0f, 531 "scaleY", 1.0f, 532 "delay", delay, 533 "onUpdate", mUpdateListener)); 534 } 535 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 536 "ease", Ease.Cubic.easeOut, 537 "alpha", 1.0f, 538 "scaleX", 1.0f, 539 "scaleY", 1.0f, 540 "delay", delay, 541 "onUpdate", mUpdateListener, 542 "onComplete", mTargetUpdateListener)); 543 544 mTargetAnimations.start(); 545 } 546 547 private void vibrate() { 548 final boolean hapticEnabled = Settings.System.getIntForUser( 549 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, 550 UserHandle.USER_CURRENT) != 0; 551 if (mVibrator != null && hapticEnabled) { 552 mVibrator.vibrate(mVibrationDuration); 553 } 554 } 555 556 private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { 557 Resources res = getContext().getResources(); 558 TypedArray array = res.obtainTypedArray(resourceId); 559 final int count = array.length(); 560 ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); 561 for (int i = 0; i < count; i++) { 562 TypedValue value = array.peekValue(i); 563 TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); 564 drawables.add(target); 565 } 566 array.recycle(); 567 return drawables; 568 } 569 570 private void internalSetTargetResources(int resourceId) { 571 final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId); 572 mTargetDrawables = targets; 573 mTargetResourceId = resourceId; 574 575 int maxWidth = mHandleDrawable.getWidth(); 576 int maxHeight = mHandleDrawable.getHeight(); 577 final int count = targets.size(); 578 for (int i = 0; i < count; i++) { 579 TargetDrawable target = targets.get(i); 580 maxWidth = Math.max(maxWidth, target.getWidth()); 581 maxHeight = Math.max(maxHeight, target.getHeight()); 582 } 583 if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { 584 mMaxTargetWidth = maxWidth; 585 mMaxTargetHeight = maxHeight; 586 requestLayout(); // required to resize layout and call updateTargetPositions() 587 } else { 588 updateTargetPositions(mWaveCenterX, mWaveCenterY); 589 updatePointCloudPosition(mWaveCenterX, mWaveCenterY); 590 } 591 } 592 593 /** 594 * Loads an array of drawables from the given resourceId. 595 * 596 * @param resourceId 597 */ 598 public void setTargetResources(int resourceId) { 599 if (mAnimatingTargets) { 600 // postpone this change until we return to the initial state 601 mNewTargetResources = resourceId; 602 } else { 603 internalSetTargetResources(resourceId); 604 } 605 } 606 607 public int getTargetResourceId() { 608 return mTargetResourceId; 609 } 610 611 /** 612 * Sets the resource id specifying the target descriptions for accessibility. 613 * 614 * @param resourceId The resource id. 615 */ 616 public void setTargetDescriptionsResourceId(int resourceId) { 617 mTargetDescriptionsResourceId = resourceId; 618 if (mTargetDescriptions != null) { 619 mTargetDescriptions.clear(); 620 } 621 } 622 623 /** 624 * Gets the resource id specifying the target descriptions for accessibility. 625 * 626 * @return The resource id. 627 */ 628 public int getTargetDescriptionsResourceId() { 629 return mTargetDescriptionsResourceId; 630 } 631 632 /** 633 * Sets the resource id specifying the target direction descriptions for accessibility. 634 * 635 * @param resourceId The resource id. 636 */ 637 public void setDirectionDescriptionsResourceId(int resourceId) { 638 mDirectionDescriptionsResourceId = resourceId; 639 if (mDirectionDescriptions != null) { 640 mDirectionDescriptions.clear(); 641 } 642 } 643 644 /** 645 * Gets the resource id specifying the target direction descriptions. 646 * 647 * @return The resource id. 648 */ 649 public int getDirectionDescriptionsResourceId() { 650 return mDirectionDescriptionsResourceId; 651 } 652 653 /** 654 * Enable or disable vibrate on touch. 655 * 656 * @param enabled 657 */ 658 public void setVibrateEnabled(boolean enabled) { 659 if (enabled && mVibrator == null) { 660 mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); 661 } else { 662 mVibrator = null; 663 } 664 } 665 666 /** 667 * Starts wave animation. 668 * 669 */ 670 public void ping() { 671 if (mFeedbackCount > 0) { 672 boolean doWaveAnimation = true; 673 final AnimationBundle waveAnimations = mWaveAnimations; 674 675 // Don't do a wave if there's already one in progress 676 if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { 677 long t = waveAnimations.get(0).animator.getCurrentPlayTime(); 678 if (t < WAVE_ANIMATION_DURATION/2) { 679 doWaveAnimation = false; 680 } 681 } 682 683 if (doWaveAnimation) { 684 startWaveAnimation(); 685 } 686 } 687 } 688 689 private void stopAndHideWaveAnimation() { 690 mWaveAnimations.cancel(); 691 mPointCloud.waveManager.setAlpha(0.0f); 692 } 693 694 private void startWaveAnimation() { 695 mWaveAnimations.cancel(); 696 mPointCloud.waveManager.setAlpha(1.0f); 697 mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); 698 mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, 699 "ease", Ease.Quad.easeOut, 700 "delay", 0, 701 "radius", 2.0f * mOuterRadius, 702 "onUpdate", mUpdateListener, 703 "onComplete", 704 new AnimatorListenerAdapter() { 705 public void onAnimationEnd(Animator animator) { 706 mPointCloud.waveManager.setRadius(0.0f); 707 mPointCloud.waveManager.setAlpha(0.0f); 708 } 709 })); 710 mWaveAnimations.start(); 711 } 712 713 /** 714 * Resets the widget to default state and cancels all animation. If animate is 'true', will 715 * animate objects into place. Otherwise, objects will snap back to place. 716 * 717 * @param animate 718 */ 719 public void reset(boolean animate) { 720 mGlowAnimations.stop(); 721 mTargetAnimations.stop(); 722 startBackgroundAnimation(0, 0.0f); 723 stopAndHideWaveAnimation(); 724 hideTargets(animate, false); 725 hideGlow(0, 0, 0.0f, null); 726 Tweener.reset(); 727 } 728 729 private void startBackgroundAnimation(int duration, float alpha) { 730 final Drawable background = getBackground(); 731 if (mAlwaysTrackFinger && background != null) { 732 if (mBackgroundAnimator != null) { 733 mBackgroundAnimator.animator.cancel(); 734 } 735 mBackgroundAnimator = Tweener.to(background, duration, 736 "ease", Ease.Cubic.easeIn, 737 "alpha", (int)(255.0f * alpha), 738 "delay", SHOW_ANIMATION_DELAY); 739 mBackgroundAnimator.animator.start(); 740 } 741 } 742 743 @Override 744 public boolean onTouchEvent(MotionEvent event) { 745 final int action = event.getActionMasked(); 746 boolean handled = false; 747 switch (action) { 748 case MotionEvent.ACTION_POINTER_DOWN: 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_POINTER_UP: 763 case MotionEvent.ACTION_UP: 764 if (DEBUG) Log.v(TAG, "*** UP ***"); 765 handleMove(event); 766 handleUp(event); 767 handled = true; 768 break; 769 770 case MotionEvent.ACTION_CANCEL: 771 if (DEBUG) Log.v(TAG, "*** CANCEL ***"); 772 handleMove(event); 773 handleCancel(event); 774 handled = true; 775 break; 776 777 } 778 invalidate(); 779 return handled ? true : super.onTouchEvent(event); 780 } 781 782 private void updateGlowPosition(float x, float y) { 783 mPointCloud.glowManager.setX(x); 784 mPointCloud.glowManager.setY(y); 785 } 786 787 private void handleDown(MotionEvent event) { 788 int actionIndex = event.getActionIndex(); 789 float eventX = event.getX(actionIndex); 790 float eventY = event.getY(actionIndex); 791 switchToState(STATE_START, eventX, eventY); 792 if (!trySwitchToFirstTouchState(eventX, eventY)) { 793 mDragging = false; 794 } else { 795 mPointerId = event.getPointerId(actionIndex); 796 updateGlowPosition(eventX, eventY); 797 } 798 } 799 800 private void handleUp(MotionEvent event) { 801 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); 802 int actionIndex = event.getActionIndex(); 803 if (event.getPointerId(actionIndex) == mPointerId) { 804 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); 805 } 806 } 807 808 private void handleCancel(MotionEvent event) { 809 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); 810 811 // Drop the active target if canceled. 812 mActiveTarget = -1; 813 814 int actionIndex = event.findPointerIndex(mPointerId); 815 actionIndex = actionIndex == -1 ? 0 : actionIndex; 816 switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); 817 } 818 819 private void handleMove(MotionEvent event) { 820 int activeTarget = -1; 821 final int historySize = event.getHistorySize(); 822 ArrayList<TargetDrawable> targets = mTargetDrawables; 823 int ntargets = targets.size(); 824 float x = 0.0f; 825 float y = 0.0f; 826 int actionIndex = event.findPointerIndex(mPointerId); 827 828 if (actionIndex == -1) { 829 return; // no data for this pointer 830 } 831 832 for (int k = 0; k < historySize + 1; k++) { 833 float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) 834 : event.getX(actionIndex); 835 float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) 836 : event.getY(actionIndex); 837 // tx and ty are relative to wave center 838 float tx = eventX - mWaveCenterX; 839 float ty = eventY - mWaveCenterY; 840 float touchRadius = (float) Math.sqrt(dist2(tx, ty)); 841 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; 842 float limitX = tx * scale; 843 float limitY = ty * scale; 844 double angleRad = Math.atan2(-ty, tx); 845 846 if (!mDragging) { 847 trySwitchToFirstTouchState(eventX, eventY); 848 } 849 850 if (mDragging) { 851 // For multiple targets, snap to the one that matches 852 final float snapRadius = mOuterRadius - mSnapMargin; 853 final float snapDistance2 = snapRadius * snapRadius; 854 // Find first target in range 855 for (int i = 0; i < ntargets; i++) { 856 TargetDrawable target = targets.get(i); 857 858 double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; 859 double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; 860 if (target.isEnabled()) { 861 boolean angleMatches = 862 (angleRad > targetMinRad && angleRad <= targetMaxRad) || 863 (angleRad + 2 * Math.PI > targetMinRad && 864 angleRad + 2 * Math.PI <= targetMaxRad); 865 if (angleMatches && (dist2(tx, ty) > snapDistance2)) { 866 activeTarget = i; 867 } 868 } 869 } 870 } 871 x = limitX; 872 y = limitY; 873 } 874 875 if (!mDragging) { 876 return; 877 } 878 879 if (activeTarget != -1) { 880 switchToState(STATE_SNAP, x,y); 881 updateGlowPosition(x, y); 882 } else { 883 switchToState(STATE_TRACKING, x, y); 884 updateGlowPosition(x, y); 885 } 886 887 if (mActiveTarget != activeTarget) { 888 // Defocus the old target 889 if (mActiveTarget != -1) { 890 TargetDrawable target = targets.get(mActiveTarget); 891 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 892 target.setState(TargetDrawable.STATE_INACTIVE); 893 } 894 } 895 // Focus the new target 896 if (activeTarget != -1) { 897 TargetDrawable target = targets.get(activeTarget); 898 if (target.hasState(TargetDrawable.STATE_FOCUSED)) { 899 target.setState(TargetDrawable.STATE_FOCUSED); 900 } 901 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 902 String targetContentDescription = getTargetDescription(activeTarget); 903 announceForAccessibility(targetContentDescription); 904 } 905 } 906 } 907 mActiveTarget = activeTarget; 908 } 909 910 @Override 911 public boolean onHoverEvent(MotionEvent event) { 912 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 913 final int action = event.getAction(); 914 switch (action) { 915 case MotionEvent.ACTION_HOVER_ENTER: 916 event.setAction(MotionEvent.ACTION_DOWN); 917 break; 918 case MotionEvent.ACTION_HOVER_MOVE: 919 event.setAction(MotionEvent.ACTION_MOVE); 920 break; 921 case MotionEvent.ACTION_HOVER_EXIT: 922 event.setAction(MotionEvent.ACTION_UP); 923 break; 924 } 925 onTouchEvent(event); 926 event.setAction(action); 927 } 928 return super.onHoverEvent(event); 929 } 930 931 /** 932 * Sets the current grabbed state, and dispatches a grabbed state change 933 * event to our listener. 934 */ 935 private void setGrabbedState(int newState) { 936 if (newState != mGrabbedState) { 937 if (newState != OnTriggerListener.NO_HANDLE) { 938 vibrate(); 939 } 940 mGrabbedState = newState; 941 if (mOnTriggerListener != null) { 942 if (newState == OnTriggerListener.NO_HANDLE) { 943 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); 944 } else { 945 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); 946 } 947 mOnTriggerListener.onGrabbedStateChange(this, newState); 948 } 949 } 950 } 951 952 private boolean trySwitchToFirstTouchState(float x, float y) { 953 final float tx = x - mWaveCenterX; 954 final float ty = y - mWaveCenterY; 955 if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { 956 if (DEBUG) Log.v(TAG, "** Handle HIT"); 957 switchToState(STATE_FIRST_TOUCH, x, y); 958 updateGlowPosition(tx, ty); 959 mDragging = true; 960 return true; 961 } 962 return false; 963 } 964 965 private void assignDefaultsIfNeeded() { 966 if (mOuterRadius == 0.0f) { 967 mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; 968 } 969 if (mSnapMargin == 0.0f) { 970 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 971 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); 972 } 973 if (mInnerRadius == 0.0f) { 974 mInnerRadius = mHandleDrawable.getWidth() / 10.0f; 975 } 976 } 977 978 private void computeInsets(int dx, int dy) { 979 final int layoutDirection = getLayoutDirection(); 980 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 981 982 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 983 case Gravity.LEFT: 984 mHorizontalInset = 0; 985 break; 986 case Gravity.RIGHT: 987 mHorizontalInset = dx; 988 break; 989 case Gravity.CENTER_HORIZONTAL: 990 default: 991 mHorizontalInset = dx / 2; 992 break; 993 } 994 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 995 case Gravity.TOP: 996 mVerticalInset = 0; 997 break; 998 case Gravity.BOTTOM: 999 mVerticalInset = dy; 1000 break; 1001 case Gravity.CENTER_VERTICAL: 1002 default: 1003 mVerticalInset = dy / 2; 1004 break; 1005 } 1006 } 1007 1008 @Override 1009 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1010 super.onLayout(changed, left, top, right, bottom); 1011 final int width = right - left; 1012 final int height = bottom - top; 1013 1014 // Target placement width/height. This puts the targets on the greater of the ring 1015 // width or the specified outer radius. 1016 final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); 1017 final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); 1018 float newWaveCenterX = mHorizontalInset 1019 + Math.max(width, mMaxTargetWidth + placementWidth) / 2; 1020 float newWaveCenterY = mVerticalInset 1021 + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; 1022 1023 if (mInitialLayout) { 1024 stopAndHideWaveAnimation(); 1025 hideTargets(false, false); 1026 mInitialLayout = false; 1027 } 1028 1029 mOuterRing.setPositionX(newWaveCenterX); 1030 mOuterRing.setPositionY(newWaveCenterY); 1031 1032 mHandleDrawable.setPositionX(newWaveCenterX); 1033 mHandleDrawable.setPositionY(newWaveCenterY); 1034 1035 updateTargetPositions(newWaveCenterX, newWaveCenterY); 1036 updatePointCloudPosition(newWaveCenterX, newWaveCenterY); 1037 updateGlowPosition(newWaveCenterX, newWaveCenterY); 1038 1039 mWaveCenterX = newWaveCenterX; 1040 mWaveCenterY = newWaveCenterY; 1041 1042 if (DEBUG) dump(); 1043 } 1044 1045 private void updateTargetPositions(float centerX, float centerY) { 1046 // Reposition the target drawables if the view changed. 1047 ArrayList<TargetDrawable> targets = mTargetDrawables; 1048 final int size = targets.size(); 1049 final float alpha = (float) (-2.0f * Math.PI / size); 1050 for (int i = 0; i < size; i++) { 1051 final TargetDrawable targetIcon = targets.get(i); 1052 final float angle = alpha * i; 1053 targetIcon.setPositionX(centerX); 1054 targetIcon.setPositionY(centerY); 1055 targetIcon.setX(mOuterRadius * (float) Math.cos(angle)); 1056 targetIcon.setY(mOuterRadius * (float) Math.sin(angle)); 1057 } 1058 } 1059 1060 private void updatePointCloudPosition(float centerX, float centerY) { 1061 mPointCloud.setCenter(centerX, centerY); 1062 } 1063 1064 @Override 1065 protected void onDraw(Canvas canvas) { 1066 mPointCloud.draw(canvas); 1067 mOuterRing.draw(canvas); 1068 final int ntargets = mTargetDrawables.size(); 1069 for (int i = 0; i < ntargets; i++) { 1070 TargetDrawable target = mTargetDrawables.get(i); 1071 if (target != null) { 1072 target.draw(canvas); 1073 } 1074 } 1075 mHandleDrawable.draw(canvas); 1076 } 1077 1078 public void setOnTriggerListener(OnTriggerListener listener) { 1079 mOnTriggerListener = listener; 1080 } 1081 1082 private float square(float d) { 1083 return d * d; 1084 } 1085 1086 private float dist2(float dx, float dy) { 1087 return dx*dx + dy*dy; 1088 } 1089 1090 private float getScaledGlowRadiusSquared() { 1091 final float scaledTapRadius; 1092 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 1093 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; 1094 } else { 1095 scaledTapRadius = mGlowRadius; 1096 } 1097 return square(scaledTapRadius); 1098 } 1099 1100 private void announceTargets() { 1101 StringBuilder utterance = new StringBuilder(); 1102 final int targetCount = mTargetDrawables.size(); 1103 for (int i = 0; i < targetCount; i++) { 1104 String targetDescription = getTargetDescription(i); 1105 String directionDescription = getDirectionDescription(i); 1106 if (!TextUtils.isEmpty(targetDescription) 1107 && !TextUtils.isEmpty(directionDescription)) { 1108 String text = String.format(directionDescription, targetDescription); 1109 utterance.append(text); 1110 } 1111 } 1112 if (utterance.length() > 0) { 1113 announceForAccessibility(utterance.toString()); 1114 } 1115 } 1116 1117 private String getTargetDescription(int index) { 1118 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { 1119 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); 1120 if (mTargetDrawables.size() != mTargetDescriptions.size()) { 1121 Log.w(TAG, "The number of target drawables must be" 1122 + " equal to the number of target descriptions."); 1123 return null; 1124 } 1125 } 1126 return mTargetDescriptions.get(index); 1127 } 1128 1129 private String getDirectionDescription(int index) { 1130 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { 1131 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); 1132 if (mTargetDrawables.size() != mDirectionDescriptions.size()) { 1133 Log.w(TAG, "The number of target drawables must be" 1134 + " equal to the number of direction descriptions."); 1135 return null; 1136 } 1137 } 1138 return mDirectionDescriptions.get(index); 1139 } 1140 1141 private ArrayList<String> loadDescriptions(int resourceId) { 1142 TypedArray array = getContext().getResources().obtainTypedArray(resourceId); 1143 final int count = array.length(); 1144 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); 1145 for (int i = 0; i < count; i++) { 1146 String contentDescription = array.getString(i); 1147 targetContentDescriptions.add(contentDescription); 1148 } 1149 array.recycle(); 1150 return targetContentDescriptions; 1151 } 1152 1153 public int getResourceIdForTarget(int index) { 1154 final TargetDrawable drawable = mTargetDrawables.get(index); 1155 return drawable == null ? 0 : drawable.getResourceId(); 1156 } 1157 1158 public void setEnableTarget(int resourceId, boolean enabled) { 1159 for (int i = 0; i < mTargetDrawables.size(); i++) { 1160 final TargetDrawable target = mTargetDrawables.get(i); 1161 if (target.getResourceId() == resourceId) { 1162 target.setEnabled(enabled); 1163 break; // should never be more than one match 1164 } 1165 } 1166 } 1167 1168 /** 1169 * Gets the position of a target in the array that matches the given resource. 1170 * @param resourceId 1171 * @return the index or -1 if not found 1172 */ 1173 public int getTargetPosition(int resourceId) { 1174 for (int i = 0; i < mTargetDrawables.size(); i++) { 1175 final TargetDrawable target = mTargetDrawables.get(i); 1176 if (target.getResourceId() == resourceId) { 1177 return i; // should never be more than one match 1178 } 1179 } 1180 return -1; 1181 } 1182 1183 private boolean replaceTargetDrawables(Resources res, int existingResourceId, 1184 int newResourceId) { 1185 if (existingResourceId == 0 || newResourceId == 0) { 1186 return false; 1187 } 1188 1189 boolean result = false; 1190 final ArrayList<TargetDrawable> drawables = mTargetDrawables; 1191 final int size = drawables.size(); 1192 for (int i = 0; i < size; i++) { 1193 final TargetDrawable target = drawables.get(i); 1194 if (target != null && target.getResourceId() == existingResourceId) { 1195 target.setDrawable(res, newResourceId); 1196 result = true; 1197 } 1198 } 1199 1200 if (result) { 1201 requestLayout(); // in case any given drawable's size changes 1202 } 1203 1204 return result; 1205 } 1206 1207 /** 1208 * Searches the given package for a resource to use to replace the Drawable on the 1209 * target with the given resource id 1210 * @param component of the .apk that contains the resource 1211 * @param name of the metadata in the .apk 1212 * @param existingResId the resource id of the target to search for 1213 * @return true if found in the given package and replaced at least one target Drawables 1214 */ 1215 public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, 1216 int existingResId) { 1217 if (existingResId == 0) return false; 1218 1219 boolean replaced = false; 1220 if (component != null) { 1221 try { 1222 PackageManager packageManager = mContext.getPackageManager(); 1223 // Look for the search icon specified in the activity meta-data 1224 Bundle metaData = packageManager.getActivityInfo( 1225 component, PackageManager.GET_META_DATA).metaData; 1226 if (metaData != null) { 1227 int iconResId = metaData.getInt(name); 1228 if (iconResId != 0) { 1229 Resources res = packageManager.getResourcesForActivity(component); 1230 replaced = replaceTargetDrawables(res, existingResId, iconResId); 1231 } 1232 } 1233 } catch (NameNotFoundException e) { 1234 Log.w(TAG, "Failed to swap drawable; " 1235 + component.flattenToShortString() + " not found", e); 1236 } catch (Resources.NotFoundException nfe) { 1237 Log.w(TAG, "Failed to swap drawable from " 1238 + component.flattenToShortString(), nfe); 1239 } 1240 } 1241 if (!replaced) { 1242 // Restore the original drawable 1243 replaceTargetDrawables(mContext.getResources(), existingResId, existingResId); 1244 } 1245 return replaced; 1246 } 1247} 1248