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