MultiWaveView.java revision 72b26c1fa25077b1f3367eb211be20b629f7b1d4
1/* 2 * Copyright (C) 2011 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.Context; 26import android.content.res.Resources; 27import android.content.res.TypedArray; 28import android.graphics.Canvas; 29import android.graphics.RectF; 30import android.os.Vibrator; 31import android.text.TextUtils; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.util.TypedValue; 35import android.view.Gravity; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.accessibility.AccessibilityEvent; 39import android.view.accessibility.AccessibilityManager; 40 41import com.android.internal.R; 42 43import java.util.ArrayList; 44 45/** 46 * A special widget containing a center and outer ring. Moving the center ring to the outer ring 47 * causes an event that can be caught by implementing OnTriggerListener. 48 */ 49public class MultiWaveView extends View { 50 private static final String TAG = "MultiWaveView"; 51 private static final boolean DEBUG = false; 52 53 // Wave state machine 54 private static final int STATE_IDLE = 0; 55 private static final int STATE_FIRST_TOUCH = 1; 56 private static final int STATE_TRACKING = 2; 57 private static final int STATE_SNAP = 3; 58 private static final int STATE_FINISH = 4; 59 60 // Animation properties. 61 private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it 62 63 public interface OnTriggerListener { 64 int NO_HANDLE = 0; 65 int CENTER_HANDLE = 1; 66 public void onGrabbed(View v, int handle); 67 public void onReleased(View v, int handle); 68 public void onTrigger(View v, int target); 69 public void onGrabbedStateChange(View v, int handle); 70 } 71 72 // Tune-able parameters 73 private static final int CHEVRON_INCREMENTAL_DELAY = 160; 74 private static final int CHEVRON_ANIMATION_DURATION = 850; 75 private static final int RETURN_TO_HOME_DELAY = 1200; 76 private static final int RETURN_TO_HOME_DURATION = 300; 77 private static final int HIDE_ANIMATION_DELAY = 200; 78 private static final int HIDE_ANIMATION_DURATION = RETURN_TO_HOME_DELAY; 79 private static final int SHOW_ANIMATION_DURATION = 0; 80 private static final int SHOW_ANIMATION_DELAY = 0; 81 private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; 82 private TimeInterpolator mChevronAnimationInterpolator = Ease.Quad.easeOut; 83 84 private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); 85 private ArrayList<TargetDrawable> mChevronDrawables = new ArrayList<TargetDrawable>(); 86 private ArrayList<Tweener> mChevronAnimations = new ArrayList<Tweener>(); 87 private ArrayList<Tweener> mTargetAnimations = new ArrayList<Tweener>(); 88 private ArrayList<String> mTargetDescriptions; 89 private ArrayList<String> mDirectionDescriptions; 90 private Tweener mHandleAnimation; 91 private OnTriggerListener mOnTriggerListener; 92 private TargetDrawable mHandleDrawable; 93 private TargetDrawable mOuterRing; 94 private Vibrator mVibrator; 95 96 private int mFeedbackCount = 3; 97 private int mVibrationDuration = 0; 98 private int mGrabbedState; 99 private int mActiveTarget = -1; 100 private float mTapRadius; 101 private float mWaveCenterX; 102 private float mWaveCenterY; 103 private int mMaxTargetHeight; 104 private int mMaxTargetWidth; 105 private float mHorizontalOffset; 106 private float mVerticalOffset; 107 108 private float mOuterRadius = 0.0f; 109 private float mHitRadius = 0.0f; 110 private float mSnapMargin = 0.0f; 111 private boolean mDragging; 112 private int mNewTargetResources; 113 114 private AnimatorListener mResetListener = new AnimatorListenerAdapter() { 115 public void onAnimationEnd(Animator animator) { 116 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 117 } 118 }; 119 120 private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { 121 public void onAnimationEnd(Animator animator) { 122 ping(); 123 switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); 124 } 125 }; 126 127 private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { 128 public void onAnimationUpdate(ValueAnimator animation) { 129 invalidateGlobalRegion(mHandleDrawable); 130 invalidate(); 131 } 132 }; 133 134 private boolean mAnimatingTargets; 135 private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { 136 public void onAnimationEnd(Animator animator) { 137 if (mNewTargetResources != 0) { 138 internalSetTargetResources(mNewTargetResources); 139 mNewTargetResources = 0; 140 hideTargets(false); 141 } 142 mAnimatingTargets = false; 143 } 144 }; 145 private int mTargetResourceId; 146 private int mTargetDescriptionsResourceId; 147 private int mDirectionDescriptionsResourceId; 148 private boolean mAlwaysTrackFinger; 149 private int mHorizontalInset; 150 private int mVerticalInset; 151 private int mGravity = Gravity.TOP; 152 153 public MultiWaveView(Context context) { 154 this(context, null); 155 } 156 157 public MultiWaveView(Context context, AttributeSet attrs) { 158 super(context, attrs); 159 Resources res = context.getResources(); 160 161 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiWaveView); 162 mOuterRadius = a.getDimension(R.styleable.MultiWaveView_outerRadius, mOuterRadius); 163// mHorizontalOffset = a.getDimension(R.styleable.MultiWaveView_horizontalOffset, 164// mHorizontalOffset); 165// mVerticalOffset = a.getDimension(R.styleable.MultiWaveView_verticalOffset, mVerticalOffset); 166 mHitRadius = a.getDimension(R.styleable.MultiWaveView_hitRadius, mHitRadius); 167 mSnapMargin = a.getDimension(R.styleable.MultiWaveView_snapMargin, mSnapMargin); 168 mVibrationDuration = a.getInt(R.styleable.MultiWaveView_vibrationDuration, 169 mVibrationDuration); 170 mFeedbackCount = a.getInt(R.styleable.MultiWaveView_feedbackCount, 171 mFeedbackCount); 172 mHandleDrawable = new TargetDrawable(res, 173 a.peekValue(R.styleable.MultiWaveView_handleDrawable).resourceId); 174 mTapRadius = mHandleDrawable.getWidth()/2; 175 mOuterRing = new TargetDrawable(res, 176 a.peekValue(R.styleable.MultiWaveView_waveDrawable).resourceId); 177 mAlwaysTrackFinger = a.getBoolean(R.styleable.MultiWaveView_alwaysTrackFinger, false); 178 mGravity = a.getInt(R.styleable.MultiWaveView_gravity, Gravity.TOP); 179 180 // Read chevron animation drawables 181 final int chevrons[] = { R.styleable.MultiWaveView_leftChevronDrawable, 182 R.styleable.MultiWaveView_rightChevronDrawable, 183 R.styleable.MultiWaveView_topChevronDrawable, 184 R.styleable.MultiWaveView_bottomChevronDrawable 185 }; 186 187 for (int chevron : chevrons) { 188 TypedValue typedValue = a.peekValue(chevron); 189 for (int i = 0; i < mFeedbackCount; i++) { 190 mChevronDrawables.add( 191 typedValue != null ? new TargetDrawable(res, typedValue.resourceId) : null); 192 } 193 } 194 195 // Read array of target drawables 196 TypedValue outValue = new TypedValue(); 197 if (a.getValue(R.styleable.MultiWaveView_targetDrawables, outValue)) { 198 internalSetTargetResources(outValue.resourceId); 199 } 200 if (mTargetDrawables == null || mTargetDrawables.size() == 0) { 201 throw new IllegalStateException("Must specify at least one target drawable"); 202 } 203 204 // Read array of target descriptions 205 if (a.getValue(R.styleable.MultiWaveView_targetDescriptions, outValue)) { 206 final int resourceId = outValue.resourceId; 207 if (resourceId == 0) { 208 throw new IllegalStateException("Must specify target descriptions"); 209 } 210 setTargetDescriptionsResourceId(resourceId); 211 } 212 213 // Read array of direction descriptions 214 if (a.getValue(R.styleable.MultiWaveView_directionDescriptions, outValue)) { 215 final int resourceId = outValue.resourceId; 216 if (resourceId == 0) { 217 throw new IllegalStateException("Must specify direction descriptions"); 218 } 219 setDirectionDescriptionsResourceId(resourceId); 220 } 221 222 a.recycle(); 223 setVibrateEnabled(mVibrationDuration > 0); 224 } 225 226 private void dump() { 227 Log.v(TAG, "Outer Radius = " + mOuterRadius); 228 Log.v(TAG, "HitRadius = " + mHitRadius); 229 Log.v(TAG, "SnapMargin = " + mSnapMargin); 230 Log.v(TAG, "FeedbackCount = " + mFeedbackCount); 231 Log.v(TAG, "VibrationDuration = " + mVibrationDuration); 232 Log.v(TAG, "TapRadius = " + mTapRadius); 233 Log.v(TAG, "WaveCenterX = " + mWaveCenterX); 234 Log.v(TAG, "WaveCenterY = " + mWaveCenterY); 235 Log.v(TAG, "HorizontalOffset = " + mHorizontalOffset); 236 Log.v(TAG, "VerticalOffset = " + mVerticalOffset); 237 } 238 239 @Override 240 protected int getSuggestedMinimumWidth() { 241 // View should be large enough to contain the background + handle and 242 // target drawable on either edge. 243 return mOuterRing.getWidth() + mMaxTargetWidth; 244 } 245 246 @Override 247 protected int getSuggestedMinimumHeight() { 248 // View should be large enough to contain the unlock ring + target and 249 // target drawable on either edge 250 return mOuterRing.getHeight() + mMaxTargetHeight; 251 } 252 253 private int resolveMeasured(int measureSpec, int desired) 254 { 255 int result = 0; 256 int specSize = MeasureSpec.getSize(measureSpec); 257 switch (MeasureSpec.getMode(measureSpec)) { 258 case MeasureSpec.UNSPECIFIED: 259 result = desired; 260 break; 261 case MeasureSpec.AT_MOST: 262 result = Math.min(specSize, desired); 263 break; 264 case MeasureSpec.EXACTLY: 265 default: 266 result = specSize; 267 } 268 return result; 269 } 270 271 @Override 272 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 273 final int minimumWidth = getSuggestedMinimumWidth(); 274 final int minimumHeight = getSuggestedMinimumHeight(); 275 int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 276 int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 277 setupGravity((computedWidth - minimumWidth), (computedHeight - minimumHeight)); 278 setMeasuredDimension(computedWidth, computedHeight); 279 } 280 281 private void switchToState(int state, float x, float y) { 282 switch (state) { 283 case STATE_IDLE: 284 deactivateTargets(); 285 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); 286 break; 287 288 case STATE_FIRST_TOUCH: 289 stopHandleAnimation(); 290 deactivateTargets(); 291 showTargets(true); 292 mHandleDrawable.setState(TargetDrawable.STATE_ACTIVE); 293 setGrabbedState(OnTriggerListener.CENTER_HANDLE); 294 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 295 announceTargets(); 296 } 297 break; 298 299 case STATE_TRACKING: 300 break; 301 302 case STATE_SNAP: 303 break; 304 305 case STATE_FINISH: 306 doFinish(); 307 break; 308 } 309 } 310 311 /** 312 * Animation used to attract user's attention to the target button. 313 * Assumes mChevronDrawables is an a list with an even number of chevrons filled with 314 * mFeedbackCount items in the order: left, right, top, bottom. 315 */ 316 private void startChevronAnimation() { 317 final float r = mHandleDrawable.getWidth() * 0.4f; 318 final float chevronAnimationDistance = mOuterRadius * 0.9f; 319 final float from[][] = { 320 {mWaveCenterX - r, mWaveCenterY}, // left 321 {mWaveCenterX + r, mWaveCenterY}, // right 322 {mWaveCenterX, mWaveCenterY - r}, // top 323 {mWaveCenterX, mWaveCenterY + r} }; // bottom 324 final float to[][] = { 325 {mWaveCenterX - chevronAnimationDistance, mWaveCenterY}, // left 326 {mWaveCenterX + chevronAnimationDistance, mWaveCenterY}, // right 327 {mWaveCenterX, mWaveCenterY - chevronAnimationDistance}, // top 328 {mWaveCenterX, mWaveCenterY + chevronAnimationDistance} }; // bottom 329 330 mChevronAnimations.clear(); 331 final float startScale = 0.5f; 332 final float endScale = 2.0f; 333 for (int direction = 0; direction < 4; direction++) { 334 for (int count = 0; count < mFeedbackCount; count++) { 335 int delay = count * CHEVRON_INCREMENTAL_DELAY; 336 final TargetDrawable icon = mChevronDrawables.get(direction*mFeedbackCount + count); 337 if (icon == null) { 338 continue; 339 } 340 mChevronAnimations.add(Tweener.to(icon, CHEVRON_ANIMATION_DURATION, 341 "ease", mChevronAnimationInterpolator, 342 "delay", delay, 343 "x", new float[] { from[direction][0], to[direction][0] }, 344 "y", new float[] { from[direction][1], to[direction][1] }, 345 "alpha", new float[] {1.0f, 0.0f}, 346 "scaleX", new float[] {startScale, endScale}, 347 "scaleY", new float[] {startScale, endScale}, 348 "onUpdate", mUpdateListener)); 349 } 350 } 351 } 352 353 private void stopChevronAnimation() { 354 for (Tweener anim : mChevronAnimations) { 355 anim.animator.end(); 356 } 357 mChevronAnimations.clear(); 358 } 359 360 private void stopHandleAnimation() { 361 if (mHandleAnimation != null) { 362 mHandleAnimation.animator.end(); 363 mHandleAnimation = null; 364 } 365 } 366 367 private void deactivateTargets() { 368 for (TargetDrawable target : mTargetDrawables) { 369 target.setState(TargetDrawable.STATE_INACTIVE); 370 } 371 mActiveTarget = -1; 372 } 373 374 void invalidateGlobalRegion(TargetDrawable drawable) { 375 int width = drawable.getWidth(); 376 int height = drawable.getHeight(); 377 RectF childBounds = new RectF(0, 0, width, height); 378 childBounds.offset(drawable.getX() - width/2, drawable.getY() - height/2); 379 View view = this; 380 while (view.getParent() != null && view.getParent() instanceof View) { 381 view = (View) view.getParent(); 382 view.getMatrix().mapRect(childBounds); 383 view.invalidate((int) Math.floor(childBounds.left), 384 (int) Math.floor(childBounds.top), 385 (int) Math.ceil(childBounds.right), 386 (int) Math.ceil(childBounds.bottom)); 387 } 388 } 389 390 /** 391 * Dispatches a trigger event to listener. Ignored if a listener is not set. 392 * @param whichHandle the handle that triggered the event. 393 */ 394 private void dispatchTriggerEvent(int whichHandle) { 395 vibrate(); 396 if (mOnTriggerListener != null) { 397 mOnTriggerListener.onTrigger(this, whichHandle); 398 } 399 } 400 401 private void dispatchGrabbedEvent(int whichHandler) { 402 vibrate(); 403 if (mOnTriggerListener != null) { 404 mOnTriggerListener.onGrabbed(this, whichHandler); 405 } 406 } 407 408 private void doFinish() { 409 final int activeTarget = mActiveTarget; 410 boolean targetHit = activeTarget != -1; 411 412 // Hide unselected targets 413 hideTargets(true); 414 415 // Highlight the selected one 416 mHandleDrawable.setAlpha(targetHit ? 0.0f : 1.0f); 417 if (targetHit) { 418 mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); 419 420 hideUnselected(activeTarget); 421 422 // Inform listener of any active targets. Typically only one will be active. 423 if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); 424 dispatchTriggerEvent(mActiveTarget); 425 mHandleAnimation = Tweener.to(mHandleDrawable, 0, 426 "ease", Ease.Quart.easeOut, 427 "delay", RETURN_TO_HOME_DELAY, 428 "alpha", 1.0f, 429 "x", mWaveCenterX, 430 "y", mWaveCenterY, 431 "onUpdate", mUpdateListener, 432 "onComplete", mResetListener); 433 } else { 434 // Animate finger outline back to home position 435 mHandleAnimation = Tweener.to(mHandleDrawable, RETURN_TO_HOME_DURATION, 436 "ease", Ease.Quart.easeOut, 437 "delay", 0, 438 "alpha", 1.0f, 439 "x", mWaveCenterX, 440 "y", mWaveCenterY, 441 "onUpdate", mUpdateListener, 442 "onComplete", mDragging ? mResetListenerWithPing : mResetListener); 443 } 444 445 setGrabbedState(OnTriggerListener.NO_HANDLE); 446 } 447 448 private void hideUnselected(int active) { 449 for (int i = 0; i < mTargetDrawables.size(); i++) { 450 if (i != active) { 451 mTargetDrawables.get(i).setAlpha(0.0f); 452 } 453 } 454 mOuterRing.setAlpha(0.0f); 455 } 456 457 private void hideTargets(boolean animate) { 458 if (mTargetAnimations.size() > 0) { 459 stopTargetAnimation(); 460 } 461 // Note: these animations should complete at the same time so that we can swap out 462 // the target assets asynchronously from the setTargetResources() call. 463 mAnimatingTargets = animate; 464 if (animate) { 465 final int duration = animate ? HIDE_ANIMATION_DURATION : 0; 466 for (TargetDrawable target : mTargetDrawables) { 467 target.setState(TargetDrawable.STATE_INACTIVE); 468 mTargetAnimations.add(Tweener.to(target, duration, 469 "alpha", 0.0f, 470 "delay", HIDE_ANIMATION_DELAY, 471 "onUpdate", mUpdateListener)); 472 } 473 mTargetAnimations.add(Tweener.to(mOuterRing, duration, 474 "alpha", 0.0f, 475 "delay", HIDE_ANIMATION_DELAY, 476 "onUpdate", mUpdateListener, 477 "onComplete", mTargetUpdateListener)); 478 } else { 479 for (TargetDrawable target : mTargetDrawables) { 480 target.setState(TargetDrawable.STATE_INACTIVE); 481 target.setAlpha(0.0f); 482 } 483 mOuterRing.setAlpha(0.0f); 484 } 485 } 486 487 private void showTargets(boolean animate) { 488 if (mTargetAnimations.size() > 0) { 489 stopTargetAnimation(); 490 } 491 mAnimatingTargets = animate; 492 if (animate) { 493 for (TargetDrawable target : mTargetDrawables) { 494 target.setState(TargetDrawable.STATE_INACTIVE); 495 mTargetAnimations.add(Tweener.to(target, SHOW_ANIMATION_DURATION, 496 "alpha", 1.0f, 497 "delay", SHOW_ANIMATION_DELAY, 498 "onUpdate", mUpdateListener)); 499 } 500 mTargetAnimations.add(Tweener.to(mOuterRing, SHOW_ANIMATION_DURATION, 501 "alpha", 1.0f, 502 "delay", SHOW_ANIMATION_DELAY, 503 "onUpdate", mUpdateListener, 504 "onComplete", mTargetUpdateListener)); 505 } else { 506 for (TargetDrawable target : mTargetDrawables) { 507 target.setState(TargetDrawable.STATE_INACTIVE); 508 target.setAlpha(1.0f); 509 } 510 mOuterRing.setAlpha(1.0f); 511 } 512 } 513 514 private void stopTargetAnimation() { 515 for (Tweener anim : mTargetAnimations) { 516 anim.animator.end(); 517 } 518 mTargetAnimations.clear(); 519 } 520 521 private void vibrate() { 522 if (mVibrator != null) { 523 mVibrator.vibrate(mVibrationDuration); 524 } 525 } 526 527 private void internalSetTargetResources(int resourceId) { 528 Resources res = getContext().getResources(); 529 TypedArray array = res.obtainTypedArray(resourceId); 530 int count = array.length(); 531 ArrayList<TargetDrawable> targetDrawables = new ArrayList<TargetDrawable>(count); 532 int maxWidth = mHandleDrawable.getWidth(); 533 int maxHeight = mHandleDrawable.getHeight(); 534 for (int i = 0; i < count; i++) { 535 TypedValue value = array.peekValue(i); 536 TargetDrawable target= new TargetDrawable(res, value != null ? value.resourceId : 0); 537 targetDrawables.add(target); 538 maxWidth = Math.max(maxWidth, target.getWidth()); 539 maxHeight = Math.max(maxHeight, target.getHeight()); 540 } 541 mTargetResourceId = resourceId; 542 mTargetDrawables = targetDrawables; 543 if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { 544 mMaxTargetWidth = maxWidth; 545 mMaxTargetHeight = maxHeight; 546 requestLayout(); // required to resize layout and call updateTargetPositions() 547 } else { 548 updateTargetPositions(); 549 } 550 array.recycle(); 551 } 552 553 /** 554 * Loads an array of drawables from the given resourceId. 555 * 556 * @param resourceId 557 */ 558 public void setTargetResources(int resourceId) { 559 if (mAnimatingTargets) { 560 // postpone this change until we return to the initial state 561 mNewTargetResources = resourceId; 562 } else { 563 internalSetTargetResources(resourceId); 564 } 565 } 566 567 public int getTargetResourceId() { 568 return mTargetResourceId; 569 } 570 571 /** 572 * Sets the resource id specifying the target descriptions for accessibility. 573 * 574 * @param resourceId The resource id. 575 */ 576 public void setTargetDescriptionsResourceId(int resourceId) { 577 mTargetDescriptionsResourceId = resourceId; 578 if (mTargetDescriptions != null) { 579 mTargetDescriptions.clear(); 580 } 581 } 582 583 /** 584 * Gets the resource id specifying the target descriptions for accessibility. 585 * 586 * @return The resource id. 587 */ 588 public int getTargetDescriptionsResourceId() { 589 return mTargetDescriptionsResourceId; 590 } 591 592 /** 593 * Sets the resource id specifying the target direction descriptions for accessibility. 594 * 595 * @param resourceId The resource id. 596 */ 597 public void setDirectionDescriptionsResourceId(int resourceId) { 598 mDirectionDescriptionsResourceId = resourceId; 599 if (mDirectionDescriptions != null) { 600 mDirectionDescriptions.clear(); 601 } 602 } 603 604 /** 605 * Gets the resource id specifying the target direction descriptions. 606 * 607 * @return The resource id. 608 */ 609 public int getDirectionDescriptionsResourceId() { 610 return mDirectionDescriptionsResourceId; 611 } 612 613 /** 614 * Enable or disable vibrate on touch. 615 * 616 * @param enabled 617 */ 618 public void setVibrateEnabled(boolean enabled) { 619 if (enabled && mVibrator == null) { 620 mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); 621 } else { 622 mVibrator = null; 623 } 624 } 625 626 /** 627 * Starts chevron animation. Example use case: show chevron animation whenever the phone rings 628 * or the user touches the screen. 629 * 630 */ 631 public void ping() { 632 stopChevronAnimation(); 633 startChevronAnimation(); 634 } 635 636 /** 637 * Resets the widget to default state and cancels all animation. If animate is 'true', will 638 * animate objects into place. Otherwise, objects will snap back to place. 639 * 640 * @param animate 641 */ 642 public void reset(boolean animate) { 643 stopChevronAnimation(); 644 stopHandleAnimation(); 645 stopTargetAnimation(); 646 hideChevrons(); 647 hideTargets(animate); 648 mHandleDrawable.setX(mWaveCenterX); 649 mHandleDrawable.setY(mWaveCenterY); 650 mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); 651 Tweener.reset(); 652 } 653 654 @Override 655 public boolean onTouchEvent(MotionEvent event) { 656 final int action = event.getAction(); 657 boolean handled = false; 658 switch (action) { 659 case MotionEvent.ACTION_DOWN: 660 if (DEBUG) Log.v(TAG, "*** DOWN ***"); 661 handleDown(event); 662 handled = true; 663 break; 664 665 case MotionEvent.ACTION_MOVE: 666 if (DEBUG) Log.v(TAG, "*** MOVE ***"); 667 handleMove(event); 668 handled = true; 669 break; 670 671 case MotionEvent.ACTION_UP: 672 if (DEBUG) Log.v(TAG, "*** UP ***"); 673 handleMove(event); 674 handleUp(event); 675 handled = true; 676 break; 677 678 case MotionEvent.ACTION_CANCEL: 679 if (DEBUG) Log.v(TAG, "*** CANCEL ***"); 680 // handleMove(event); 681 handleCancel(event); 682 handled = true; 683 break; 684 } 685 invalidate(); 686 return handled ? true : super.onTouchEvent(event); 687 } 688 689 private void moveHandleTo(float x, float y, boolean animate) { 690 // TODO: animate the handle based on the current state/position 691 mHandleDrawable.setX(x); 692 mHandleDrawable.setY(y); 693 } 694 695 private void handleDown(MotionEvent event) { 696 if (!trySwitchToFirstTouchState(event)) { 697 mDragging = false; 698 stopTargetAnimation(); 699 ping(); 700 } 701 } 702 703 private void handleUp(MotionEvent event) { 704 if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); 705 switchToState(STATE_FINISH, event.getX(), event.getY()); 706 } 707 708 private void handleCancel(MotionEvent event) { 709 if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); 710 mActiveTarget = -1; // Drop the active target if canceled. 711 switchToState(STATE_FINISH, event.getX(), event.getY()); 712 } 713 714 private void handleMove(MotionEvent event) { 715 if (!mDragging) { 716 trySwitchToFirstTouchState(event); 717 return; 718 } 719 720 int activeTarget = -1; 721 final int historySize = event.getHistorySize(); 722 for (int k = 0; k < historySize + 1; k++) { 723 float x = k < historySize ? event.getHistoricalX(k) : event.getX(); 724 float y = k < historySize ? event.getHistoricalY(k) : event.getY(); 725 float tx = x - mWaveCenterX; 726 float ty = y - mWaveCenterY; 727 float touchRadius = (float) Math.sqrt(dist2(tx, ty)); 728 final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; 729 float limitX = mWaveCenterX + tx * scale; 730 float limitY = mWaveCenterY + ty * scale; 731 732 boolean singleTarget = mTargetDrawables.size() == 1; 733 if (singleTarget) { 734 // Snap to outer ring if there's only one target 735 float snapRadius = mOuterRadius - mSnapMargin; 736 if (touchRadius > snapRadius) { 737 activeTarget = 0; 738 x = limitX; 739 y = limitY; 740 } 741 } else { 742 // If there's more than one target, snap to the closest one less than hitRadius away. 743 float best = Float.MAX_VALUE; 744 final float hitRadius2 = mHitRadius * mHitRadius; 745 for (int i = 0; i < mTargetDrawables.size(); i++) { 746 // Snap to the first target in range 747 TargetDrawable target = mTargetDrawables.get(i); 748 float dx = limitX - target.getX(); 749 float dy = limitY - target.getY(); 750 float dist2 = dx*dx + dy*dy; 751 if (target.isEnabled() && dist2 < hitRadius2 && dist2 < best) { 752 activeTarget = i; 753 best = dist2; 754 } 755 } 756 x = limitX; 757 y = limitY; 758 } 759 if (activeTarget != -1) { 760 switchToState(STATE_SNAP, x,y); 761 float newX = singleTarget ? limitX : mTargetDrawables.get(activeTarget).getX(); 762 float newY = singleTarget ? limitY : mTargetDrawables.get(activeTarget).getY(); 763 moveHandleTo(newX, newY, false); 764 TargetDrawable currentTarget = mTargetDrawables.get(activeTarget); 765 if (currentTarget.hasState(TargetDrawable.STATE_FOCUSED)) { 766 currentTarget.setState(TargetDrawable.STATE_FOCUSED); 767 mHandleDrawable.setAlpha(0.0f); 768 } 769 } else { 770 switchToState(STATE_TRACKING, x, y); 771 moveHandleTo(x, y, false); 772 mHandleDrawable.setAlpha(1.0f); 773 } 774 } 775 776 // Draw handle outside parent's bounds 777 invalidateGlobalRegion(mHandleDrawable); 778 779 if (mActiveTarget != activeTarget && activeTarget != -1) { 780 dispatchGrabbedEvent(activeTarget); 781 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 782 String targetContentDescription = getTargetDescription(activeTarget); 783 announceText(targetContentDescription); 784 } 785 } 786 mActiveTarget = activeTarget; 787 } 788 789 @Override 790 public boolean onHoverEvent(MotionEvent event) { 791 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 792 final int action = event.getAction(); 793 switch (action) { 794 case MotionEvent.ACTION_HOVER_ENTER: 795 event.setAction(MotionEvent.ACTION_DOWN); 796 break; 797 case MotionEvent.ACTION_HOVER_MOVE: 798 event.setAction(MotionEvent.ACTION_MOVE); 799 break; 800 case MotionEvent.ACTION_HOVER_EXIT: 801 event.setAction(MotionEvent.ACTION_UP); 802 break; 803 } 804 onTouchEvent(event); 805 event.setAction(action); 806 } 807 return super.onHoverEvent(event); 808 } 809 810 /** 811 * Sets the current grabbed state, and dispatches a grabbed state change 812 * event to our listener. 813 */ 814 private void setGrabbedState(int newState) { 815 if (newState != mGrabbedState) { 816 if (newState != OnTriggerListener.NO_HANDLE) { 817 vibrate(); 818 } 819 mGrabbedState = newState; 820 if (mOnTriggerListener != null) { 821 if (newState == OnTriggerListener.NO_HANDLE) { 822 mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); 823 } else { 824 mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); 825 } 826 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); 827 } 828 } 829 } 830 831 private boolean trySwitchToFirstTouchState(MotionEvent event) { 832 final float x = event.getX(); 833 final float y = event.getY(); 834 final float dx = x - mWaveCenterX; 835 final float dy = y - mWaveCenterY; 836 if (mAlwaysTrackFinger || dist2(dx,dy) <= getScaledTapRadiusSquared()) { 837 if (DEBUG) Log.v(TAG, "** Handle HIT"); 838 switchToState(STATE_FIRST_TOUCH, x, y); 839 moveHandleTo(x, y, false); 840 mDragging = true; 841 return true; 842 } 843 return false; 844 } 845 846 private void performInitialLayout(float centerX, float centerY) { 847 if (mOuterRadius == 0.0f) { 848 mOuterRadius = 0.5f*(float) Math.sqrt(dist2(centerX, centerY)); 849 } 850 if (mHitRadius == 0.0f) { 851 // Use the radius of inscribed circle of the first target. 852 mHitRadius = mTargetDrawables.get(0).getWidth() / 2.0f; 853 } 854 if (mSnapMargin == 0.0f) { 855 mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 856 SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); 857 } 858 hideChevrons(); 859 hideTargets(false); 860 moveHandleTo(centerX, centerY, false); 861 } 862 863 private void setupGravity(int dx, int dy) { 864 final int layoutDirection = getResolvedLayoutDirection(); 865 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 866 867 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 868 case Gravity.LEFT: 869 mHorizontalInset = 0; 870 break; 871 case Gravity.RIGHT: 872 mHorizontalInset = dx; 873 break; 874 case Gravity.CENTER_HORIZONTAL: 875 default: 876 mHorizontalInset = dx / 2; 877 break; 878 } 879 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 880 case Gravity.TOP: 881 mVerticalInset = 0; 882 break; 883 case Gravity.BOTTOM: 884 mVerticalInset = dy; 885 break; 886 case Gravity.CENTER_VERTICAL: 887 default: 888 mVerticalInset = dy / 2; 889 break; 890 } 891 } 892 893 @Override 894 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 895 super.onLayout(changed, left, top, right, bottom); 896 final int width = right - left; 897 final int height = bottom - top; 898 float newWaveCenterX = mHorizontalOffset + mHorizontalInset 899 + Math.max(width, mMaxTargetWidth + mOuterRing.getWidth()) / 2; 900 float newWaveCenterY = mVerticalOffset + mVerticalInset 901 + Math.max(height, + mMaxTargetHeight + mOuterRing.getHeight()) / 2; 902 if (newWaveCenterX != mWaveCenterX || newWaveCenterY != mWaveCenterY) { 903 if (mWaveCenterX == 0 && mWaveCenterY == 0) { 904 performInitialLayout(newWaveCenterX, newWaveCenterY); 905 } 906 mWaveCenterX = newWaveCenterX; 907 mWaveCenterY = newWaveCenterY; 908 909 mOuterRing.setX(mWaveCenterX); 910 mOuterRing.setY(Math.max(mWaveCenterY, mWaveCenterY)); 911 } 912 updateTargetPositions(); 913 if (DEBUG) dump(); 914 } 915 916 private void updateTargetPositions() { 917 // Reposition the target drawables if the view changed. 918 for (int i = 0; i < mTargetDrawables.size(); i++) { 919 final TargetDrawable targetIcon = mTargetDrawables.get(i); 920 double angle = -2.0f * Math.PI * i / mTargetDrawables.size(); 921 float xPosition = mWaveCenterX + mOuterRadius * (float) Math.cos(angle); 922 float yPosition = mWaveCenterY + mOuterRadius * (float) Math.sin(angle); 923 targetIcon.setX(xPosition); 924 targetIcon.setY(yPosition); 925 } 926 } 927 928 private void hideChevrons() { 929 for (TargetDrawable chevron : mChevronDrawables) { 930 if (chevron != null) { 931 chevron.setAlpha(0.0f); 932 } 933 } 934 } 935 936 @Override 937 protected void onDraw(Canvas canvas) { 938 mOuterRing.draw(canvas); 939 for (TargetDrawable target : mTargetDrawables) { 940 if (target != null) { 941 target.draw(canvas); 942 } 943 } 944 for (TargetDrawable target : mChevronDrawables) { 945 if (target != null) { 946 target.draw(canvas); 947 } 948 } 949 mHandleDrawable.draw(canvas); 950 } 951 952 public void setOnTriggerListener(OnTriggerListener listener) { 953 mOnTriggerListener = listener; 954 } 955 956 private float square(float d) { 957 return d * d; 958 } 959 960 private float dist2(float dx, float dy) { 961 return dx*dx + dy*dy; 962 } 963 964 private float getScaledTapRadiusSquared() { 965 final float scaledTapRadius; 966 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 967 scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mTapRadius; 968 } else { 969 scaledTapRadius = mTapRadius; 970 } 971 return square(scaledTapRadius); 972 } 973 974 private void announceTargets() { 975 StringBuilder utterance = new StringBuilder(); 976 final int targetCount = mTargetDrawables.size(); 977 for (int i = 0; i < targetCount; i++) { 978 String targetDescription = getTargetDescription(i); 979 String directionDescription = getDirectionDescription(i); 980 if (!TextUtils.isEmpty(targetDescription) 981 && !TextUtils.isEmpty(directionDescription)) { 982 String text = String.format(directionDescription, targetDescription); 983 utterance.append(text); 984 } 985 if (utterance.length() > 0) { 986 announceText(utterance.toString()); 987 } 988 } 989 } 990 991 private void announceText(String text) { 992 setContentDescription(text); 993 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 994 setContentDescription(null); 995 } 996 997 private String getTargetDescription(int index) { 998 if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { 999 mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); 1000 if (mTargetDrawables.size() != mTargetDescriptions.size()) { 1001 Log.w(TAG, "The number of target drawables must be" 1002 + " euqal to the number of target descriptions."); 1003 return null; 1004 } 1005 } 1006 return mTargetDescriptions.get(index); 1007 } 1008 1009 private String getDirectionDescription(int index) { 1010 if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { 1011 mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); 1012 if (mTargetDrawables.size() != mDirectionDescriptions.size()) { 1013 Log.w(TAG, "The number of target drawables must be" 1014 + " euqal to the number of direction descriptions."); 1015 return null; 1016 } 1017 } 1018 return mDirectionDescriptions.get(index); 1019 } 1020 1021 private ArrayList<String> loadDescriptions(int resourceId) { 1022 TypedArray array = getContext().getResources().obtainTypedArray(resourceId); 1023 final int count = array.length(); 1024 ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); 1025 for (int i = 0; i < count; i++) { 1026 String contentDescription = array.getString(i); 1027 targetContentDescriptions.add(contentDescription); 1028 } 1029 array.recycle(); 1030 return targetContentDescriptions; 1031 } 1032 1033 public int getResourceIdForTarget(int index) { 1034 final TargetDrawable drawable = mTargetDrawables.get(index); 1035 return drawable == null ? 0 : drawable.getResourceId(); 1036 } 1037 1038 public void setEnableTarget(int resourceId, boolean enabled) { 1039 for (int i = 0; i < mTargetDrawables.size(); i++) { 1040 final TargetDrawable target = mTargetDrawables.get(i); 1041 if (target.getResourceId() == resourceId) { 1042 target.setEnabled(enabled); 1043 break; // should never be more than one match 1044 } 1045 } 1046 } 1047 1048 /** 1049 * Gets the position of a target in the array that matches the given resource. 1050 * @param resourceId 1051 * @return the index or -1 if not found 1052 */ 1053 public int getTargetPosition(int resourceId) { 1054 for (int i = 0; i < mTargetDrawables.size(); i++) { 1055 final TargetDrawable target = mTargetDrawables.get(i); 1056 if (target.getResourceId() == resourceId) { 1057 return i; // should never be more than one match 1058 } 1059 } 1060 return -1; 1061 } 1062} 1063