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