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