1 /* 2 * Copyright (C) 2016 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 17 package com.android.incallui.answer.impl.answermethod; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.PropertyValuesHolder; 24 import android.animation.ValueAnimator; 25 import android.annotation.SuppressLint; 26 import android.content.Context; 27 import android.content.res.ColorStateList; 28 import android.graphics.PorterDuff.Mode; 29 import android.graphics.drawable.Drawable; 30 import android.os.Bundle; 31 import android.support.annotation.ColorInt; 32 import android.support.annotation.FloatRange; 33 import android.support.annotation.IntDef; 34 import android.support.annotation.NonNull; 35 import android.support.annotation.Nullable; 36 import android.support.annotation.VisibleForTesting; 37 import android.support.v4.graphics.ColorUtils; 38 import android.support.v4.view.animation.FastOutLinearInInterpolator; 39 import android.support.v4.view.animation.FastOutSlowInInterpolator; 40 import android.support.v4.view.animation.LinearOutSlowInInterpolator; 41 import android.support.v4.view.animation.PathInterpolatorCompat; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.View.AccessibilityDelegate; 46 import android.view.ViewGroup; 47 import android.view.accessibility.AccessibilityNodeInfo; 48 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 49 import android.view.animation.BounceInterpolator; 50 import android.view.animation.DecelerateInterpolator; 51 import android.view.animation.Interpolator; 52 import android.widget.ImageView; 53 import android.widget.TextView; 54 import com.android.dialer.common.DpUtil; 55 import com.android.dialer.common.LogUtil; 56 import com.android.dialer.common.MathUtil; 57 import com.android.dialer.util.DrawableConverter; 58 import com.android.dialer.util.ViewUtil; 59 import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnProgressChangedListener; 60 import com.android.incallui.answer.impl.classifier.FalsingManager; 61 import com.android.incallui.answer.impl.hint.AnswerHint; 62 import com.android.incallui.answer.impl.hint.AnswerHintFactory; 63 import com.android.incallui.answer.impl.hint.PawImageLoaderImpl; 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 67 /** Answer method that swipes up to answer or down to reject. */ 68 @SuppressLint("ClickableViewAccessibility") 69 public class FlingUpDownMethod extends AnswerMethod implements OnProgressChangedListener { 70 71 private static final float SWIPE_LERP_PROGRESS_FACTOR = 0.5f; 72 private static final long ANIMATE_DURATION_SHORT_MILLIS = 667; 73 private static final long ANIMATE_DURATION_NORMAL_MILLIS = 1_333; 74 private static final long ANIMATE_DURATION_LONG_MILLIS = 1_500; 75 private static final long BOUNCE_ANIMATION_DELAY = 167; 76 private static final long VIBRATION_TIME_MILLIS = 1_833; 77 private static final long SETTLE_ANIMATION_DURATION_MILLIS = 100; 78 private static final int HINT_JUMP_DP = 60; 79 private static final int HINT_DIP_DP = 8; 80 private static final float HINT_SCALE_RATIO = 1.15f; 81 private static final long SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS = 333; 82 private static final int HINT_REJECT_SHOW_DURATION_MILLIS = 2000; 83 private static final int ICON_END_CALL_ROTATION_DEGREES = 135; 84 private static final int HINT_REJECT_FADE_TRANSLATION_Y_DP = -8; 85 private static final float SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP = 150; 86 private static final int SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP = 24; 87 88 @Retention(RetentionPolicy.SOURCE) 89 @IntDef( 90 value = { 91 AnimationState.NONE, 92 AnimationState.ENTRY, 93 AnimationState.BOUNCE, 94 AnimationState.SWIPE, 95 AnimationState.SETTLE, 96 AnimationState.HINT, 97 AnimationState.COMPLETED 98 } 99 ) 100 @VisibleForTesting 101 @interface AnimationState { 102 103 int NONE = 0; 104 int ENTRY = 1; // Entry animation for incoming call 105 int BOUNCE = 2; // An idle state in which text and icon slightly bounces off its base repeatedly 106 int SWIPE = 3; // A special state in which text and icon follows the finger movement 107 int SETTLE = 4; // A short animation to reset from swipe and prepare for hint or bounce 108 int HINT = 5; // Jump animation to suggest what to do 109 int COMPLETED = 6; // Animation loop completed. Occurs after user swipes beyond threshold 110 } 111 moveTowardY(View view, float newY)112 private static void moveTowardY(View view, float newY) { 113 view.setTranslationY(MathUtil.lerp(view.getTranslationY(), newY, SWIPE_LERP_PROGRESS_FACTOR)); 114 } 115 moveTowardX(View view, float newX)116 private static void moveTowardX(View view, float newX) { 117 view.setTranslationX(MathUtil.lerp(view.getTranslationX(), newX, SWIPE_LERP_PROGRESS_FACTOR)); 118 } 119 fadeToward(View view, float newAlpha)120 private static void fadeToward(View view, float newAlpha) { 121 view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, SWIPE_LERP_PROGRESS_FACTOR)); 122 } 123 rotateToward(View view, float newRotation)124 private static void rotateToward(View view, float newRotation) { 125 view.setRotation(MathUtil.lerp(view.getRotation(), newRotation, SWIPE_LERP_PROGRESS_FACTOR)); 126 } 127 128 private TextView swipeToAnswerText; 129 private TextView swipeToRejectText; 130 private View contactPuckContainer; 131 private ImageView contactPuckBackground; 132 private ImageView contactPuckIcon; 133 private View incomingDisconnectText; 134 private View spaceHolder; 135 private Animator lockBounceAnim; 136 private AnimatorSet lockEntryAnim; 137 private AnimatorSet lockHintAnim; 138 private AnimatorSet lockSettleAnim; 139 @AnimationState private int animationState = AnimationState.NONE; 140 @AnimationState private int afterSettleAnimationState = AnimationState.NONE; 141 // a value for finger swipe progress. -1 or less for "reject"; 1 or more for "accept". 142 private float swipeProgress; 143 private Animator rejectHintHide; 144 private Animator vibrationAnimator; 145 private Drawable contactPhoto; 146 private boolean incomingWillDisconnect; 147 private FlingUpDownTouchHandler touchHandler; 148 private FalsingManager falsingManager; 149 150 private AnswerHint answerHint; 151 152 @Override onCreate(@ullable Bundle bundle)153 public void onCreate(@Nullable Bundle bundle) { 154 super.onCreate(bundle); 155 falsingManager = new FalsingManager(getContext()); 156 } 157 158 @Override onStart()159 public void onStart() { 160 super.onStart(); 161 falsingManager.onScreenOn(); 162 if (getView() != null) { 163 if (animationState == AnimationState.SWIPE || animationState == AnimationState.HINT) { 164 swipeProgress = 0; 165 updateContactPuck(); 166 onMoveReset(false); 167 } else if (animationState == AnimationState.ENTRY) { 168 // When starting from the lock screen, the activity may be stopped and started briefly. 169 // Don't let that interrupt the entry animation 170 startSwipeToAnswerEntryAnimation(); 171 } 172 } 173 } 174 175 @Override onStop()176 public void onStop() { 177 endAnimation(); 178 falsingManager.onScreenOff(); 179 if (getActivity().isFinishing()) { 180 setAnimationState(AnimationState.COMPLETED); 181 } 182 super.onStop(); 183 } 184 185 @Nullable 186 @Override onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)187 public View onCreateView( 188 LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { 189 View view = layoutInflater.inflate(R.layout.swipe_up_down_method, viewGroup, false); 190 191 contactPuckContainer = view.findViewById(R.id.incoming_call_puck_container); 192 contactPuckBackground = (ImageView) view.findViewById(R.id.incoming_call_puck_bg); 193 contactPuckIcon = (ImageView) view.findViewById(R.id.incoming_call_puck_icon); 194 swipeToAnswerText = (TextView) view.findViewById(R.id.incoming_swipe_to_answer_text); 195 swipeToRejectText = (TextView) view.findViewById(R.id.incoming_swipe_to_reject_text); 196 incomingDisconnectText = view.findViewById(R.id.incoming_will_disconnect_text); 197 incomingDisconnectText.setVisibility(incomingWillDisconnect ? View.VISIBLE : View.GONE); 198 incomingDisconnectText.setAlpha(incomingWillDisconnect ? 1 : 0); 199 spaceHolder = view.findViewById(R.id.incoming_bouncer_space_holder); 200 spaceHolder.setVisibility(incomingWillDisconnect ? View.GONE : View.VISIBLE); 201 202 view.findViewById(R.id.incoming_swipe_to_answer_container) 203 .setAccessibilityDelegate( 204 new AccessibilityDelegate() { 205 @Override 206 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 207 super.onInitializeAccessibilityNodeInfo(host, info); 208 info.addAction( 209 new AccessibilityAction( 210 R.id.accessibility_action_answer, 211 getString(R.string.call_incoming_answer))); 212 info.addAction( 213 new AccessibilityAction( 214 R.id.accessibility_action_decline, 215 getString(R.string.call_incoming_decline))); 216 } 217 218 @Override 219 public boolean performAccessibilityAction(View host, int action, Bundle args) { 220 if (action == R.id.accessibility_action_answer) { 221 performAccept(); 222 return true; 223 } else if (action == R.id.accessibility_action_decline) { 224 performReject(); 225 return true; 226 } 227 return super.performAccessibilityAction(host, action, args); 228 } 229 }); 230 231 swipeProgress = 0; 232 233 updateContactPuck(); 234 235 touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager); 236 237 answerHint = 238 new AnswerHintFactory(new PawImageLoaderImpl()) 239 .create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY); 240 answerHint.onCreateView( 241 layoutInflater, 242 (ViewGroup) view.findViewById(R.id.hint_container), 243 contactPuckContainer, 244 swipeToAnswerText); 245 return view; 246 } 247 248 @Override onViewCreated(View view, @Nullable Bundle bundle)249 public void onViewCreated(View view, @Nullable Bundle bundle) { 250 super.onViewCreated(view, bundle); 251 setAnimationState(AnimationState.ENTRY); 252 } 253 254 @Override onDestroyView()255 public void onDestroyView() { 256 super.onDestroyView(); 257 if (touchHandler != null) { 258 touchHandler.detach(); 259 touchHandler = null; 260 } 261 } 262 263 @Override onProgressChanged(@loatRangefrom = -1f, to = 1f) float progress)264 public void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress) { 265 swipeProgress = progress; 266 if (animationState == AnimationState.SWIPE && getContext() != null && isVisible()) { 267 updateSwipeTextAndPuckForTouch(); 268 } 269 } 270 271 @Override onTrackingStart()272 public void onTrackingStart() { 273 setAnimationState(AnimationState.SWIPE); 274 } 275 276 @Override onTrackingStopped()277 public void onTrackingStopped() {} 278 279 @Override onMoveReset(boolean showHint)280 public void onMoveReset(boolean showHint) { 281 if (showHint) { 282 showSwipeHint(); 283 } else { 284 setAnimationState(AnimationState.BOUNCE); 285 } 286 resetTouchState(); 287 getParent().resetAnswerProgress(); 288 } 289 290 @Override onMoveFinish(boolean accept)291 public void onMoveFinish(boolean accept) { 292 touchHandler.setTouchEnabled(false); 293 answerHint.onAnswered(); 294 if (accept) { 295 performAccept(); 296 } else { 297 performReject(); 298 } 299 } 300 301 @Override shouldUseFalsing(@onNull MotionEvent downEvent)302 public boolean shouldUseFalsing(@NonNull MotionEvent downEvent) { 303 if (contactPuckContainer == null) { 304 return false; 305 } 306 307 float puckCenterX = contactPuckContainer.getX() + (contactPuckContainer.getWidth() / 2); 308 float puckCenterY = contactPuckContainer.getY() + (contactPuckContainer.getHeight() / 2); 309 double radius = contactPuckContainer.getHeight() / 2; 310 311 // Squaring a number is more performant than taking a sqrt, so we compare the square of the 312 // distance with the square of the radius. 313 double distSq = 314 Math.pow(downEvent.getX() - puckCenterX, 2) + Math.pow(downEvent.getY() - puckCenterY, 2); 315 return distSq >= Math.pow(radius, 2); 316 } 317 318 @Override setContactPhoto(Drawable contactPhoto)319 public void setContactPhoto(Drawable contactPhoto) { 320 this.contactPhoto = contactPhoto; 321 322 updateContactPuck(); 323 } 324 updateContactPuck()325 private void updateContactPuck() { 326 if (contactPuckIcon == null) { 327 return; 328 } 329 if (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) { 330 contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24); 331 } else { 332 contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24); 333 } 334 335 int size = 336 contactPuckBackground 337 .getResources() 338 .getDimensionPixelSize( 339 shouldShowPhotoInPuck() 340 ? R.dimen.answer_contact_puck_size_photo 341 : R.dimen.answer_contact_puck_size_no_photo); 342 contactPuckBackground.setImageDrawable( 343 shouldShowPhotoInPuck() 344 ? makeRoundedDrawable(contactPuckBackground.getContext(), contactPhoto, size) 345 : null); 346 ViewGroup.LayoutParams contactPuckParams = contactPuckBackground.getLayoutParams(); 347 contactPuckParams.height = size; 348 contactPuckParams.width = size; 349 contactPuckBackground.setLayoutParams(contactPuckParams); 350 contactPuckIcon.setAlpha(shouldShowPhotoInPuck() ? 0f : 1f); 351 } 352 makeRoundedDrawable(Context context, Drawable contactPhoto, int size)353 private Drawable makeRoundedDrawable(Context context, Drawable contactPhoto, int size) { 354 return DrawableConverter.getRoundedDrawable(context, contactPhoto, size, size); 355 } 356 shouldShowPhotoInPuck()357 private boolean shouldShowPhotoInPuck() { 358 return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) 359 && contactPhoto != null; 360 } 361 362 @Override setHintText(@ullable CharSequence hintText)363 public void setHintText(@Nullable CharSequence hintText) { 364 if (hintText == null) { 365 swipeToAnswerText.setText(R.string.call_incoming_swipe_to_answer); 366 swipeToRejectText.setText(R.string.call_incoming_swipe_to_reject); 367 } else { 368 swipeToAnswerText.setText(hintText); 369 swipeToRejectText.setText(null); 370 } 371 } 372 373 @Override setShowIncomingWillDisconnect(boolean incomingWillDisconnect)374 public void setShowIncomingWillDisconnect(boolean incomingWillDisconnect) { 375 this.incomingWillDisconnect = incomingWillDisconnect; 376 if (incomingDisconnectText != null) { 377 if (incomingWillDisconnect) { 378 incomingDisconnectText.setVisibility(View.VISIBLE); 379 spaceHolder.setVisibility(View.GONE); 380 incomingDisconnectText.animate().alpha(1); 381 } else { 382 incomingDisconnectText 383 .animate() 384 .alpha(0) 385 .setListener( 386 new AnimatorListenerAdapter() { 387 @Override 388 public void onAnimationEnd(Animator animation) { 389 super.onAnimationEnd(animation); 390 incomingDisconnectText.setVisibility(View.GONE); 391 spaceHolder.setVisibility(View.VISIBLE); 392 } 393 }); 394 } 395 } 396 } 397 showSwipeHint()398 private void showSwipeHint() { 399 setAnimationState(AnimationState.HINT); 400 } 401 updateSwipeTextAndPuckForTouch()402 private void updateSwipeTextAndPuckForTouch() { 403 // Clamp progress value between -1 and 1. 404 final float clampedProgress = MathUtil.clamp(swipeProgress, -1 /* min */, 1 /* max */); 405 final float positiveAdjustedProgress = Math.abs(clampedProgress); 406 final boolean isAcceptingFlow = clampedProgress >= 0; 407 408 // Cancel view property animators on views we're about to mutate 409 swipeToAnswerText.animate().cancel(); 410 contactPuckIcon.animate().cancel(); 411 412 // Since the animation progression is controlled by user gesture instead of real timeline, the 413 // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec. 414 // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline. 415 // 416 // See specs - 417 // Accept: https://direct.googleplex.com/#/spec/8510001 418 // Decline: https://direct.googleplex.com/#/spec/3850001 419 final float progressSlots = 9; 420 421 // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade. 422 float swipeTextAlpha = Math.max(0, 1 - Math.abs(clampedProgress) * progressSlots); 423 fadeToward(swipeToAnswerText, swipeTextAlpha); 424 // Fade out the "swipe down to dismiss" at the same time. Don't ever increase its alpha 425 fadeToward(swipeToRejectText, Math.min(swipeTextAlpha, swipeToRejectText.getAlpha())); 426 // Fade out the "incoming will disconnect" text 427 fadeToward(incomingDisconnectText, incomingWillDisconnect ? swipeTextAlpha : 0); 428 429 // Move swipe text back to zero. 430 moveTowardX(swipeToAnswerText, 0 /* newX */); 431 moveTowardY(swipeToAnswerText, 0 /* newY */); 432 433 // Animate puck color 434 @ColorInt 435 int destPuckColor = 436 getContext() 437 .getColor( 438 isAcceptingFlow ? R.color.call_accept_background : R.color.call_hangup_background); 439 destPuckColor = 440 ColorUtils.setAlphaComponent(destPuckColor, (int) (0xFF * positiveAdjustedProgress)); 441 contactPuckBackground.setBackgroundTintList(ColorStateList.valueOf(destPuckColor)); 442 contactPuckBackground.setBackgroundTintMode(Mode.SRC_ATOP); 443 contactPuckBackground.setColorFilter(destPuckColor); 444 445 // Animate decline icon 446 if (isAcceptingFlow || getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) { 447 rotateToward(contactPuckIcon, 0f); 448 } else { 449 rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES); 450 } 451 452 // Fade in icon 453 if (shouldShowPhotoInPuck()) { 454 fadeToward(contactPuckIcon, positiveAdjustedProgress); 455 } 456 float iconProgress = Math.min(1f, positiveAdjustedProgress * 4); 457 @ColorInt 458 int iconColor = 459 ColorUtils.setAlphaComponent( 460 contactPuckIcon.getContext().getColor(R.color.incoming_answer_icon), 461 (int) (0xFF * (1 - iconProgress))); 462 contactPuckIcon.setImageTintList(ColorStateList.valueOf(iconColor)); 463 464 // Move puck. 465 if (isAcceptingFlow) { 466 moveTowardY( 467 contactPuckContainer, 468 -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP)); 469 } else { 470 moveTowardY( 471 contactPuckContainer, 472 -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP)); 473 } 474 475 getParent().onAnswerProgressUpdate(clampedProgress); 476 } 477 startSwipeToAnswerSwipeAnimation()478 private void startSwipeToAnswerSwipeAnimation() { 479 LogUtil.i("FlingUpDownMethod.startSwipeToAnswerSwipeAnimation", "Start swipe animation."); 480 resetTouchState(); 481 endAnimation(); 482 } 483 setPuckTouchState()484 private void setPuckTouchState() { 485 contactPuckBackground.setActivated(touchHandler.isTracking()); 486 } 487 resetTouchState()488 private void resetTouchState() { 489 if (getContext() == null) { 490 // State will be reset in onStart(), so just abort. 491 return; 492 } 493 contactPuckContainer.animate().scaleX(1 /* scaleX */); 494 contactPuckContainer.animate().scaleY(1 /* scaleY */); 495 contactPuckBackground.animate().scaleX(1 /* scaleX */); 496 contactPuckBackground.animate().scaleY(1 /* scaleY */); 497 contactPuckBackground.setBackgroundTintList(null); 498 contactPuckBackground.setColorFilter(null); 499 contactPuckIcon.setImageTintList( 500 ColorStateList.valueOf(getContext().getColor(R.color.incoming_answer_icon))); 501 contactPuckIcon.animate().rotation(0); 502 503 getParent().resetAnswerProgress(); 504 setPuckTouchState(); 505 506 final float alpha = 1; 507 swipeToAnswerText.animate().alpha(alpha); 508 contactPuckContainer.animate().alpha(alpha); 509 contactPuckBackground.animate().alpha(alpha); 510 contactPuckIcon.animate().alpha(shouldShowPhotoInPuck() ? 0 : alpha); 511 } 512 513 @VisibleForTesting setAnimationState(@nimationState int state)514 void setAnimationState(@AnimationState int state) { 515 if (state != AnimationState.HINT && animationState == state) { 516 return; 517 } 518 519 if (animationState == AnimationState.COMPLETED) { 520 LogUtil.e( 521 "FlingUpDownMethod.setAnimationState", 522 "Animation loop has completed. Cannot switch to new state: " + state); 523 return; 524 } 525 526 if (state == AnimationState.HINT || state == AnimationState.BOUNCE) { 527 if (animationState == AnimationState.SWIPE) { 528 afterSettleAnimationState = state; 529 state = AnimationState.SETTLE; 530 } 531 } 532 533 LogUtil.i("FlingUpDownMethod.setAnimationState", "animation state: " + state); 534 animationState = state; 535 536 // Start animation after the current one is finished completely. 537 View view = getView(); 538 if (view != null) { 539 // As long as the fragment is added, we can start update the animation state. 540 if (isAdded() && (animationState == state)) { 541 updateAnimationState(); 542 } else { 543 endAnimation(); 544 } 545 } 546 } 547 548 @AnimationState 549 @VisibleForTesting getAnimationState()550 int getAnimationState() { 551 return animationState; 552 } 553 updateAnimationState()554 private void updateAnimationState() { 555 switch (animationState) { 556 case AnimationState.ENTRY: 557 startSwipeToAnswerEntryAnimation(); 558 break; 559 case AnimationState.BOUNCE: 560 startSwipeToAnswerBounceAnimation(); 561 break; 562 case AnimationState.SWIPE: 563 startSwipeToAnswerSwipeAnimation(); 564 break; 565 case AnimationState.SETTLE: 566 startSwipeToAnswerSettleAnimation(); 567 break; 568 case AnimationState.COMPLETED: 569 clearSwipeToAnswerUi(); 570 break; 571 case AnimationState.HINT: 572 startSwipeToAnswerHintAnimation(); 573 break; 574 case AnimationState.NONE: 575 default: 576 LogUtil.e( 577 "FlingUpDownMethod.updateAnimationState", 578 "Unexpected animation state: " + animationState); 579 break; 580 } 581 } 582 startSwipeToAnswerEntryAnimation()583 private void startSwipeToAnswerEntryAnimation() { 584 LogUtil.i("FlingUpDownMethod.startSwipeToAnswerEntryAnimation", "Swipe entry animation."); 585 endAnimation(); 586 587 lockEntryAnim = new AnimatorSet(); 588 Animator textUp = 589 ObjectAnimator.ofFloat( 590 swipeToAnswerText, 591 View.TRANSLATION_Y, 592 DpUtil.dpToPx(getContext(), 192 /* dp */), 593 DpUtil.dpToPx(getContext(), -20 /* dp */)); 594 textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 595 textUp.setInterpolator(new LinearOutSlowInInterpolator()); 596 597 Animator textDown = 598 ObjectAnimator.ofFloat( 599 swipeToAnswerText, 600 View.TRANSLATION_Y, 601 DpUtil.dpToPx(getContext(), -20) /* dp */, 602 0 /* end pos */); 603 textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 604 textUp.setInterpolator(new FastOutSlowInInterpolator()); 605 606 // "Swipe down to reject" text fades in with a slight translation 607 swipeToRejectText.setAlpha(0f); 608 Animator rejectTextShow = 609 ObjectAnimator.ofPropertyValuesHolder( 610 swipeToRejectText, 611 PropertyValuesHolder.ofFloat(View.ALPHA, 1f), 612 PropertyValuesHolder.ofFloat( 613 View.TRANSLATION_Y, 614 DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP), 615 0f)); 616 rejectTextShow.setInterpolator(new FastOutLinearInInterpolator()); 617 rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS); 618 rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS); 619 620 Animator puckUp = 621 ObjectAnimator.ofFloat( 622 contactPuckContainer, 623 View.TRANSLATION_Y, 624 DpUtil.dpToPx(getContext(), 400 /* dp */), 625 DpUtil.dpToPx(getContext(), -12 /* dp */)); 626 puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS); 627 puckUp.setInterpolator( 628 PathInterpolatorCompat.create( 629 0 /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */)); 630 631 Animator puckDown = 632 ObjectAnimator.ofFloat( 633 contactPuckContainer, 634 View.TRANSLATION_Y, 635 DpUtil.dpToPx(getContext(), -12 /* dp */), 636 0 /* end pos */); 637 puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 638 puckDown.setInterpolator(new FastOutSlowInInterpolator()); 639 640 Animator puckScaleUp = 641 createUniformScaleAnimators( 642 contactPuckBackground, 643 0.33f /* beginScale */, 644 1.1f /* endScale */, 645 ANIMATE_DURATION_NORMAL_MILLIS, 646 PathInterpolatorCompat.create( 647 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */)); 648 Animator puckScaleDown = 649 createUniformScaleAnimators( 650 contactPuckBackground, 651 1.1f /* beginScale */, 652 1 /* endScale */, 653 ANIMATE_DURATION_NORMAL_MILLIS, 654 new FastOutSlowInInterpolator()); 655 656 // Upward animation chain. 657 lockEntryAnim.play(textUp).with(puckScaleUp).with(puckUp); 658 659 // Downward animation chain. 660 lockEntryAnim.play(textDown).with(puckDown).with(puckScaleDown).after(puckUp); 661 662 lockEntryAnim.play(rejectTextShow).after(puckUp); 663 664 // Add vibration animation. 665 addVibrationAnimator(lockEntryAnim); 666 667 lockEntryAnim.addListener( 668 new AnimatorListenerAdapter() { 669 670 public boolean canceled; 671 672 @Override 673 public void onAnimationCancel(Animator animation) { 674 super.onAnimationCancel(animation); 675 canceled = true; 676 } 677 678 @Override 679 public void onAnimationEnd(Animator animation) { 680 super.onAnimationEnd(animation); 681 if (!canceled) { 682 onEntryAnimationDone(); 683 } 684 } 685 }); 686 lockEntryAnim.start(); 687 } 688 689 @VisibleForTesting onEntryAnimationDone()690 void onEntryAnimationDone() { 691 LogUtil.i("FlingUpDownMethod.onEntryAnimationDone", "Swipe entry anim ends."); 692 if (animationState == AnimationState.ENTRY) { 693 setAnimationState(AnimationState.BOUNCE); 694 } 695 } 696 startSwipeToAnswerBounceAnimation()697 private void startSwipeToAnswerBounceAnimation() { 698 LogUtil.i("FlingUpDownMethod.startSwipeToAnswerBounceAnimation", "Swipe bounce animation."); 699 endAnimation(); 700 701 if (ViewUtil.areAnimationsDisabled(getContext())) { 702 swipeToAnswerText.setTranslationY(0); 703 contactPuckContainer.setTranslationY(0); 704 contactPuckBackground.setScaleY(1f); 705 contactPuckBackground.setScaleX(1f); 706 swipeToRejectText.setAlpha(1f); 707 swipeToRejectText.setTranslationY(0); 708 return; 709 } 710 711 lockBounceAnim = createBreatheAnimation(); 712 713 answerHint.onBounceStart(); 714 lockBounceAnim.addListener( 715 new AnimatorListenerAdapter() { 716 boolean firstPass = true; 717 718 @Override 719 public void onAnimationEnd(Animator animation) { 720 super.onAnimationEnd(animation); 721 if (getContext() != null 722 && lockBounceAnim != null 723 && animationState == AnimationState.BOUNCE) { 724 // AnimatorSet doesn't have repeat settings. Instead, we start a new one after the 725 // previous set is completed, until endAnimation is called. 726 LogUtil.v("FlingUpDownMethod.onAnimationEnd", "Bounce again."); 727 728 // If this is the first time repeating the animation, we should recreate it so its 729 // starting values will be correct 730 if (firstPass) { 731 lockBounceAnim = createBreatheAnimation(); 732 lockBounceAnim.addListener(this); 733 } 734 firstPass = false; 735 answerHint.onBounceStart(); 736 lockBounceAnim.start(); 737 } 738 } 739 }); 740 lockBounceAnim.start(); 741 } 742 createBreatheAnimation()743 private Animator createBreatheAnimation() { 744 AnimatorSet breatheAnimation = new AnimatorSet(); 745 float textOffset = DpUtil.dpToPx(getContext(), 42 /* dp */); 746 Animator textUp = 747 ObjectAnimator.ofFloat( 748 swipeToAnswerText, View.TRANSLATION_Y, 0 /* begin pos */, -textOffset); 749 textUp.setInterpolator(new FastOutSlowInInterpolator()); 750 textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 751 752 Animator textDown = 753 ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset, 0 /* end pos */); 754 textDown.setInterpolator(new FastOutSlowInInterpolator()); 755 textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 756 757 // "Swipe down to reject" text fade in 758 Animator rejectTextShow = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 1f); 759 rejectTextShow.setInterpolator(new LinearOutSlowInInterpolator()); 760 rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS); 761 rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS); 762 763 // reject hint text translate in 764 Animator rejectTextTranslate = 765 ObjectAnimator.ofFloat( 766 swipeToRejectText, 767 View.TRANSLATION_Y, 768 DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP), 769 0f); 770 rejectTextTranslate.setInterpolator(new FastOutSlowInInterpolator()); 771 rejectTextTranslate.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 772 773 // reject hint text fade out 774 Animator rejectTextHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0f); 775 rejectTextHide.setInterpolator(new FastOutLinearInInterpolator()); 776 rejectTextHide.setDuration(ANIMATE_DURATION_SHORT_MILLIS); 777 778 Interpolator curve = 779 PathInterpolatorCompat.create( 780 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */); 781 float puckOffset = DpUtil.dpToPx(getContext(), 42 /* dp */); 782 Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -puckOffset); 783 puckUp.setInterpolator(curve); 784 puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS); 785 786 final float scale = 1.0625f; 787 Animator puckScaleUp = 788 createUniformScaleAnimators( 789 contactPuckBackground, 790 1 /* beginScale */, 791 scale, 792 ANIMATE_DURATION_NORMAL_MILLIS, 793 curve); 794 795 Animator puckDown = 796 ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0 /* end pos */); 797 puckDown.setInterpolator(new FastOutSlowInInterpolator()); 798 puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS); 799 800 Animator puckScaleDown = 801 createUniformScaleAnimators( 802 contactPuckBackground, 803 scale, 804 1 /* endScale */, 805 ANIMATE_DURATION_NORMAL_MILLIS, 806 new FastOutSlowInInterpolator()); 807 808 // Bounce upward animation chain. 809 breatheAnimation 810 .play(textUp) 811 .with(rejectTextHide) 812 .with(puckUp) 813 .with(puckScaleUp) 814 .after(167 /* delay */); 815 816 // Bounce downward animation chain. 817 breatheAnimation 818 .play(puckDown) 819 .with(textDown) 820 .with(puckScaleDown) 821 .with(rejectTextShow) 822 .with(rejectTextTranslate) 823 .after(puckUp); 824 825 // Add vibration animation to the animator set. 826 addVibrationAnimator(breatheAnimation); 827 828 return breatheAnimation; 829 } 830 startSwipeToAnswerSettleAnimation()831 private void startSwipeToAnswerSettleAnimation() { 832 endAnimation(); 833 834 ObjectAnimator puckScale = 835 ObjectAnimator.ofPropertyValuesHolder( 836 contactPuckBackground, 837 PropertyValuesHolder.ofFloat(View.SCALE_X, 1), 838 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1)); 839 puckScale.setDuration(SETTLE_ANIMATION_DURATION_MILLIS); 840 841 ObjectAnimator iconRotation = ObjectAnimator.ofFloat(contactPuckIcon, View.ROTATION, 0); 842 iconRotation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS); 843 844 ObjectAnimator swipeToAnswerTextFade = 845 createFadeAnimation(swipeToAnswerText, 1, SETTLE_ANIMATION_DURATION_MILLIS); 846 847 ObjectAnimator contactPuckContainerFade = 848 createFadeAnimation(contactPuckContainer, 1, SETTLE_ANIMATION_DURATION_MILLIS); 849 850 ObjectAnimator contactPuckBackgroundFade = 851 createFadeAnimation(contactPuckBackground, 1, SETTLE_ANIMATION_DURATION_MILLIS); 852 853 ObjectAnimator contactPuckIconFade = 854 createFadeAnimation( 855 contactPuckIcon, shouldShowPhotoInPuck() ? 0 : 1, SETTLE_ANIMATION_DURATION_MILLIS); 856 857 ObjectAnimator contactPuckTranslation = 858 ObjectAnimator.ofPropertyValuesHolder( 859 contactPuckContainer, 860 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0), 861 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0)); 862 contactPuckTranslation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS); 863 864 lockSettleAnim = new AnimatorSet(); 865 lockSettleAnim 866 .play(puckScale) 867 .with(iconRotation) 868 .with(swipeToAnswerTextFade) 869 .with(contactPuckContainerFade) 870 .with(contactPuckBackgroundFade) 871 .with(contactPuckIconFade) 872 .with(contactPuckTranslation); 873 874 lockSettleAnim.addListener( 875 new AnimatorListenerAdapter() { 876 @Override 877 public void onAnimationCancel(Animator animation) { 878 afterSettleAnimationState = AnimationState.NONE; 879 } 880 881 @Override 882 public void onAnimationEnd(Animator animation) { 883 onSettleAnimationDone(); 884 } 885 }); 886 887 lockSettleAnim.start(); 888 } 889 890 @VisibleForTesting onSettleAnimationDone()891 void onSettleAnimationDone() { 892 if (afterSettleAnimationState != AnimationState.NONE) { 893 int nextState = afterSettleAnimationState; 894 afterSettleAnimationState = AnimationState.NONE; 895 lockSettleAnim = null; 896 897 setAnimationState(nextState); 898 } 899 } 900 createFadeAnimation(View target, float targetAlpha, long duration)901 private ObjectAnimator createFadeAnimation(View target, float targetAlpha, long duration) { 902 ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(target, View.ALPHA, targetAlpha); 903 objectAnimator.setDuration(duration); 904 return objectAnimator; 905 } 906 startSwipeToAnswerHintAnimation()907 private void startSwipeToAnswerHintAnimation() { 908 if (rejectHintHide != null) { 909 rejectHintHide.cancel(); 910 } 911 912 endAnimation(); 913 resetTouchState(); 914 915 if (ViewUtil.areAnimationsDisabled(getContext())) { 916 onHintAnimationDone(false); 917 return; 918 } 919 920 lockHintAnim = new AnimatorSet(); 921 float jumpOffset = DpUtil.dpToPx(getContext(), HINT_JUMP_DP); 922 float dipOffset = DpUtil.dpToPx(getContext(), HINT_DIP_DP); 923 float scaleSize = HINT_SCALE_RATIO; 924 float textOffset = jumpOffset + (scaleSize - 1) * contactPuckBackground.getHeight(); 925 int shortAnimTime = 926 getContext().getResources().getInteger(android.R.integer.config_shortAnimTime); 927 int mediumAnimTime = 928 getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime); 929 930 // Puck squashes to anticipate jump 931 ObjectAnimator puckAnticipate = 932 ObjectAnimator.ofPropertyValuesHolder( 933 contactPuckContainer, 934 PropertyValuesHolder.ofFloat(View.SCALE_Y, .95f), 935 PropertyValuesHolder.ofFloat(View.SCALE_X, 1.05f)); 936 puckAnticipate.setRepeatCount(1); 937 puckAnticipate.setRepeatMode(ValueAnimator.REVERSE); 938 puckAnticipate.setDuration(shortAnimTime / 2); 939 puckAnticipate.setInterpolator(new DecelerateInterpolator()); 940 puckAnticipate.addListener( 941 new AnimatorListenerAdapter() { 942 @Override 943 public void onAnimationStart(Animator animation) { 944 super.onAnimationStart(animation); 945 contactPuckContainer.setPivotY(contactPuckContainer.getHeight()); 946 } 947 948 @Override 949 public void onAnimationEnd(Animator animation) { 950 super.onAnimationEnd(animation); 951 contactPuckContainer.setPivotY(contactPuckContainer.getHeight() / 2); 952 } 953 }); 954 955 // Ensure puck is at the right starting point for the jump 956 ObjectAnimator puckResetTranslation = 957 ObjectAnimator.ofPropertyValuesHolder( 958 contactPuckContainer, 959 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0), 960 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0)); 961 puckResetTranslation.setDuration(shortAnimTime / 2); 962 puckAnticipate.setInterpolator(new DecelerateInterpolator()); 963 964 Animator textUp = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset); 965 textUp.setInterpolator(new LinearOutSlowInInterpolator()); 966 textUp.setDuration(shortAnimTime); 967 968 Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -jumpOffset); 969 puckUp.setInterpolator(new LinearOutSlowInInterpolator()); 970 puckUp.setDuration(shortAnimTime); 971 972 Animator puckScaleUp = 973 createUniformScaleAnimators( 974 contactPuckBackground, 1f, scaleSize, shortAnimTime, new LinearOutSlowInInterpolator()); 975 976 Animator rejectHintShow = 977 ObjectAnimator.ofPropertyValuesHolder( 978 swipeToRejectText, 979 PropertyValuesHolder.ofFloat(View.ALPHA, 1f), 980 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)); 981 rejectHintShow.setDuration(shortAnimTime); 982 983 Animator rejectHintDip = 984 ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, dipOffset); 985 rejectHintDip.setInterpolator(new LinearOutSlowInInterpolator()); 986 rejectHintDip.setDuration(shortAnimTime); 987 988 Animator textDown = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, 0); 989 textDown.setInterpolator(new LinearOutSlowInInterpolator()); 990 textDown.setDuration(mediumAnimTime); 991 992 Animator puckDown = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0); 993 BounceInterpolator bounce = new BounceInterpolator(); 994 puckDown.setInterpolator(bounce); 995 puckDown.setDuration(mediumAnimTime); 996 997 Animator puckScaleDown = 998 createUniformScaleAnimators( 999 contactPuckBackground, scaleSize, 1f, shortAnimTime, new LinearOutSlowInInterpolator()); 1000 1001 Animator rejectHintUp = ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, 0); 1002 rejectHintUp.setInterpolator(new LinearOutSlowInInterpolator()); 1003 rejectHintUp.setDuration(mediumAnimTime); 1004 1005 lockHintAnim.play(puckAnticipate).with(puckResetTranslation).before(puckUp); 1006 lockHintAnim 1007 .play(textUp) 1008 .with(puckUp) 1009 .with(puckScaleUp) 1010 .with(rejectHintDip) 1011 .with(rejectHintShow); 1012 lockHintAnim.play(textDown).with(puckDown).with(puckScaleDown).with(rejectHintUp).after(puckUp); 1013 lockHintAnim.start(); 1014 1015 rejectHintHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0); 1016 rejectHintHide.setStartDelay(HINT_REJECT_SHOW_DURATION_MILLIS); 1017 rejectHintHide.addListener( 1018 new AnimatorListenerAdapter() { 1019 1020 private boolean canceled; 1021 1022 @Override 1023 public void onAnimationCancel(Animator animation) { 1024 super.onAnimationCancel(animation); 1025 canceled = true; 1026 rejectHintHide = null; 1027 } 1028 1029 @Override 1030 public void onAnimationEnd(Animator animation) { 1031 super.onAnimationEnd(animation); 1032 onHintAnimationDone(canceled); 1033 } 1034 }); 1035 rejectHintHide.start(); 1036 } 1037 1038 @VisibleForTesting onHintAnimationDone(boolean canceled)1039 void onHintAnimationDone(boolean canceled) { 1040 if (!canceled && animationState == AnimationState.HINT) { 1041 setAnimationState(AnimationState.BOUNCE); 1042 } 1043 rejectHintHide = null; 1044 } 1045 clearSwipeToAnswerUi()1046 private void clearSwipeToAnswerUi() { 1047 LogUtil.i("FlingUpDownMethod.clearSwipeToAnswerUi", "Clear swipe animation."); 1048 endAnimation(); 1049 swipeToAnswerText.setVisibility(View.GONE); 1050 contactPuckContainer.setVisibility(View.GONE); 1051 } 1052 endAnimation()1053 private void endAnimation() { 1054 LogUtil.i("FlingUpDownMethod.endAnimation", "End animations."); 1055 if (lockSettleAnim != null) { 1056 lockSettleAnim.cancel(); 1057 lockSettleAnim = null; 1058 } 1059 if (lockBounceAnim != null) { 1060 lockBounceAnim.cancel(); 1061 lockBounceAnim = null; 1062 } 1063 if (lockEntryAnim != null) { 1064 lockEntryAnim.cancel(); 1065 lockEntryAnim = null; 1066 } 1067 if (lockHintAnim != null) { 1068 lockHintAnim.cancel(); 1069 lockHintAnim = null; 1070 } 1071 if (rejectHintHide != null) { 1072 rejectHintHide.cancel(); 1073 rejectHintHide = null; 1074 } 1075 if (vibrationAnimator != null) { 1076 vibrationAnimator.end(); 1077 vibrationAnimator = null; 1078 } 1079 answerHint.onBounceEnd(); 1080 } 1081 1082 // Create an animator to scale on X/Y directions uniformly. createUniformScaleAnimators( View target, float begin, float end, long duration, Interpolator interpolator)1083 private Animator createUniformScaleAnimators( 1084 View target, float begin, float end, long duration, Interpolator interpolator) { 1085 ObjectAnimator animator = 1086 ObjectAnimator.ofPropertyValuesHolder( 1087 target, 1088 PropertyValuesHolder.ofFloat(View.SCALE_X, begin, end), 1089 PropertyValuesHolder.ofFloat(View.SCALE_Y, begin, end)); 1090 animator.setDuration(duration); 1091 animator.setInterpolator(interpolator); 1092 return animator; 1093 } 1094 addVibrationAnimator(AnimatorSet animatorSet)1095 private void addVibrationAnimator(AnimatorSet animatorSet) { 1096 if (vibrationAnimator != null) { 1097 vibrationAnimator.end(); 1098 } 1099 1100 // Note that we animate the value between 0 and 1, but internally VibrateInterpolator will 1101 // translate it into actually X translation value. 1102 vibrationAnimator = 1103 ObjectAnimator.ofFloat( 1104 contactPuckContainer, View.TRANSLATION_X, 0 /* begin value */, 1 /* end value */); 1105 vibrationAnimator.setDuration(VIBRATION_TIME_MILLIS); 1106 vibrationAnimator.setInterpolator(new VibrateInterpolator(getContext())); 1107 1108 animatorSet.play(vibrationAnimator).after(0 /* delay */); 1109 } 1110 performAccept()1111 private void performAccept() { 1112 LogUtil.i("FlingUpDownMethod.performAccept", null); 1113 swipeToAnswerText.setVisibility(View.GONE); 1114 contactPuckContainer.setVisibility(View.GONE); 1115 1116 // Complete the animation loop. 1117 setAnimationState(AnimationState.COMPLETED); 1118 getParent().answerFromMethod(); 1119 } 1120 performReject()1121 private void performReject() { 1122 LogUtil.i("FlingUpDownMethod.performReject", null); 1123 swipeToAnswerText.setVisibility(View.GONE); 1124 contactPuckContainer.setVisibility(View.GONE); 1125 1126 // Complete the animation loop. 1127 setAnimationState(AnimationState.COMPLETED); 1128 getParent().rejectFromMethod(); 1129 } 1130 1131 /** Custom interpolator class for puck vibration. */ 1132 private static class VibrateInterpolator implements Interpolator { 1133 1134 private static final long RAMP_UP_BEGIN_MS = 583; 1135 private static final long RAMP_UP_DURATION_MS = 167; 1136 private static final long RAMP_UP_END_MS = RAMP_UP_BEGIN_MS + RAMP_UP_DURATION_MS; 1137 private static final long RAMP_DOWN_BEGIN_MS = 1_583; 1138 private static final long RAMP_DOWN_DURATION_MS = 250; 1139 private static final long RAMP_DOWN_END_MS = RAMP_DOWN_BEGIN_MS + RAMP_DOWN_DURATION_MS; 1140 private static final long RAMP_TOTAL_TIME_MS = RAMP_DOWN_END_MS; 1141 private final float ampMax; 1142 private final float freqMax = 80; 1143 private Interpolator sliderInterpolator = new FastOutSlowInInterpolator(); 1144 VibrateInterpolator(Context context)1145 VibrateInterpolator(Context context) { 1146 ampMax = DpUtil.dpToPx(context, 1 /* dp */); 1147 } 1148 1149 @Override getInterpolation(float t)1150 public float getInterpolation(float t) { 1151 float slider = 0; 1152 float time = t * RAMP_TOTAL_TIME_MS; 1153 1154 // Calculate the slider value based on RAMP_UP and RAMP_DOWN times. Between RAMP_UP and 1155 // RAMP_DOWN, the slider remains the maximum value of 1. 1156 if (time > RAMP_UP_BEGIN_MS && time < RAMP_UP_END_MS) { 1157 // Ramp up. 1158 slider = 1159 sliderInterpolator.getInterpolation( 1160 (time - RAMP_UP_BEGIN_MS) / (float) RAMP_UP_DURATION_MS); 1161 } else if ((time >= RAMP_UP_END_MS) && time <= RAMP_DOWN_BEGIN_MS) { 1162 // Vibrate at maximum 1163 slider = 1; 1164 } else if (time > RAMP_DOWN_BEGIN_MS && time < RAMP_DOWN_END_MS) { 1165 // Ramp down. 1166 slider = 1167 1 1168 - sliderInterpolator.getInterpolation( 1169 (time - RAMP_DOWN_BEGIN_MS) / (float) RAMP_DOWN_DURATION_MS); 1170 } 1171 1172 float ampNormalized = ampMax * slider; 1173 float freqNormalized = freqMax * slider; 1174 1175 return (float) (ampNormalized * Math.sin(time * freqNormalized)); 1176 } 1177 } 1178 } 1179