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.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 
    113   private static void moveTowardY(View view, float newY) {
    114     view.setTranslationY(MathUtil.lerp(view.getTranslationY(), newY, SWIPE_LERP_PROGRESS_FACTOR));
    115   }
    116 
    117   private static void moveTowardX(View view, float newX) {
    118     view.setTranslationX(MathUtil.lerp(view.getTranslationX(), newX, SWIPE_LERP_PROGRESS_FACTOR));
    119   }
    120 
    121   private static void fadeToward(View view, float newAlpha) {
    122     view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, SWIPE_LERP_PROGRESS_FACTOR));
    123   }
    124 
    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
    154   public void onCreate(@Nullable Bundle bundle) {
    155     super.onCreate(bundle);
    156     falsingManager = new FalsingManager(getContext());
    157   }
    158 
    159   @Override
    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
    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
    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
    256   public void onViewCreated(View view, @Nullable Bundle bundle) {
    257     super.onViewCreated(view, bundle);
    258     setAnimationState(AnimationState.ENTRY);
    259   }
    260 
    261   @Override
    262   public void onDestroyView() {
    263     super.onDestroyView();
    264     if (touchHandler != null) {
    265       touchHandler.detach();
    266       touchHandler = null;
    267     }
    268   }
    269 
    270   @Override
    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
    279   public void onTrackingStart() {
    280     setAnimationState(AnimationState.SWIPE);
    281   }
    282 
    283   @Override
    284   public void onTrackingStopped() {}
    285 
    286   @Override
    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
    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
    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
    326   public void setContactPhoto(Drawable contactPhoto) {
    327     this.contactPhoto = contactPhoto;
    328 
    329     updateContactPuck();
    330   }
    331 
    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_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 
    362   private Drawable makeRoundedDrawable(Context context, Drawable contactPhoto, int size) {
    363     return DrawableConverter.getRoundedDrawable(context, contactPhoto, size, size);
    364   }
    365 
    366   private boolean shouldShowPhotoInPuck() {
    367     return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest())
    368         && contactPhoto != null;
    369   }
    370 
    371   @Override
    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
    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 
    406   private void showSwipeHint() {
    407     setAnimationState(AnimationState.HINT);
    408   }
    409 
    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 
    486   private void startSwipeToAnswerSwipeAnimation() {
    487     LogUtil.i("FlingUpDownMethod.startSwipeToAnswerSwipeAnimation", "Start swipe animation.");
    488     resetTouchState();
    489     endAnimation();
    490   }
    491 
    492   private void setPuckTouchState() {
    493     contactPuckBackground.setActivated(touchHandler.isTracking());
    494   }
    495 
    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
    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
    558   int getAnimationState() {
    559     return animationState;
    560   }
    561 
    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 
    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
    698   void onEntryAnimationDone() {
    699     LogUtil.i("FlingUpDownMethod.onEntryAnimationDone", "Swipe entry anim ends.");
    700     if (animationState == AnimationState.ENTRY) {
    701       setAnimationState(AnimationState.BOUNCE);
    702     }
    703   }
    704 
    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 
    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 
    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
    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 
    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 
    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
   1047   void onHintAnimationDone(boolean canceled) {
   1048     if (!canceled && animationState == AnimationState.HINT) {
   1049       setAnimationState(AnimationState.BOUNCE);
   1050     }
   1051     rejectHintHide = null;
   1052   }
   1053 
   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 
   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.
   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 
   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 
   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 
   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 
   1153     VibrateInterpolator(Context context) {
   1154       ampMax = DpUtil.dpToPx(context, 1 /* dp */);
   1155     }
   1156 
   1157     @Override
   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