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