1 /*
2  * Copyright (C) 2018 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.car.notification;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.annotation.IntDef;
24 import android.content.Context;
25 import android.os.Build;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.view.ViewPropertyAnimator;
29 
30 import com.android.car.notification.template.CarNotificationBaseViewHolder;
31 
32 import java.lang.annotation.Retention;
33 
34 /** A general animation tool kit to dismiss {@link CarNotificationBaseViewHolder} */
35 class DismissAnimationHelper {
36     private static final String TAG = "CarDismissHelper";
37     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
38     /**
39      * The weight of how much swipe distance plays on the alpha value of the view.
40      * A weight of 1F will make the view completely transparent if the swipe distance is larger
41      * than the view width.
42      */
43     private static final float SWIPE_DISTANCE_WEIGHT_ON_ALPHA = 0.9F;
44     private final DismissCallback mCallBacks;
45 
46     /**
47      * The direction of motion.
48      * <ol>
49      * <li> LEFT means swiping to the left.
50      * <li> RIGHT means swiping to the right.
51      * </ol>
52      */
53     @Retention(SOURCE)
54     @IntDef({Direction.LEFT, Direction.RIGHT})
55     public @interface Direction {
56         int LEFT = 1;
57         int RIGHT = 2;
58     }
59 
60     /**
61      * The percentage of the view holder's width a non-dismissible view holder is allow to translate
62      * during a swipe gesture. As gesture's delta x distance grows the view holder should translate
63      * asymptotically to this amount.
64      */
65     private final float mMaxPercentageOfWidthWithResistance;
66 
67     /**
68      * The callback indicating the supplied view has been dismissed.
69      */
70     interface DismissCallback {
71 
72         /**
73          * Called after animation ends and the view is considered dismissed.
74          */
onDismiss(CarNotificationBaseViewHolder viewHolder)75         void onDismiss(CarNotificationBaseViewHolder viewHolder);
76     }
77 
DismissAnimationHelper(Context context, DismissCallback callbacks)78     DismissAnimationHelper(Context context, DismissCallback callbacks) {
79         mCallBacks = callbacks;
80 
81         mMaxPercentageOfWidthWithResistance =
82                 context.getResources().getFloat(R.dimen.max_percentage_of_width_with_resistance);
83     }
84 
85     /** Animate the dismissal of the given item. The velocityX is assumed to be 0. */
animateDismiss(CarNotificationBaseViewHolder viewHolder, @Direction int swipeDirection)86     void animateDismiss(CarNotificationBaseViewHolder viewHolder,
87             @Direction int swipeDirection) {
88         animateDismiss(viewHolder, swipeDirection, 0f);
89     }
90 
91     /** Animate the dismissal of the given item. */
animateDismiss( CarNotificationBaseViewHolder viewHolder, @Direction int swipeDirection, float velocityX)92     void animateDismiss(
93             CarNotificationBaseViewHolder viewHolder,
94             @Direction int swipeDirection,
95             float velocityX) {
96         if (DEBUG) {
97             Log.d(TAG, "animateDismiss direction=" + swipeDirection + " velocityX=" + velocityX);
98         }
99 
100         viewHolder.setIsAnimating(true);
101 
102         int viewWidth = viewHolder.itemView.getWidth();
103         ViewPropertyAnimator viewPropertyAnimator = viewHolder.itemView.animate()
104                 .translationX(swipeDirection == Direction.RIGHT ? viewWidth : -viewWidth)
105                 .alpha(0);
106 
107         new Handler().postDelayed(() -> {
108             viewHolder.setIsAnimating(false);
109             mCallBacks.onDismiss(viewHolder);
110         }, viewPropertyAnimator.getDuration());
111         viewPropertyAnimator.start();
112     }
113 
114     /** Animate the restore back of the given item back to it's initial state. */
animateRestore(CarNotificationBaseViewHolder viewHolder, float velocityX)115     void animateRestore(CarNotificationBaseViewHolder viewHolder, float velocityX) {
116         if (DEBUG) {
117             Log.d(TAG, "animateRestore velocityX=" + velocityX);
118         }
119         viewHolder.setIsAnimating(true);
120 
121         viewHolder.itemView.animate()
122                 .translationX(0)
123                 .alpha(1)
124                 .setListener(new AnimatorListenerAdapter() {
125                     @Override
126                     public void onAnimationEnd(Animator animation) {
127                         viewHolder.setIsAnimating(false);
128                     }
129                 });
130     }
131 
calculateAlphaValue(CarNotificationBaseViewHolder viewHolder, float translateX)132     float calculateAlphaValue(CarNotificationBaseViewHolder viewHolder, float translateX) {
133         if (!viewHolder.isDismissible() || translateX == 0) {
134             return 1F;
135         }
136 
137         int width = viewHolder.itemView.getWidth();
138         return SWIPE_DISTANCE_WEIGHT_ON_ALPHA * (1 - Math.min(Math.abs(translateX / width), 1))
139                 + (1 - SWIPE_DISTANCE_WEIGHT_ON_ALPHA);
140     }
141 
calculateTranslateDistance(CarNotificationBaseViewHolder viewHolder, float moveDeltaX)142     float calculateTranslateDistance(CarNotificationBaseViewHolder viewHolder, float moveDeltaX) {
143         // If we can dismiss then translate the same distance the touch event moved and if delta
144         // x is 0 just return 0.
145         if (viewHolder.isDismissible() || moveDeltaX == 0) {
146             return moveDeltaX;
147         }
148 
149         // Calculate possible drag resistance.
150         int swipeDirection = moveDeltaX > 0 ? Direction.RIGHT : Direction.LEFT;
151 
152         int width = viewHolder.itemView.getWidth();
153         float maxSwipeDistanceWithResistance = mMaxPercentageOfWidthWithResistance * width;
154         if (Math.abs(moveDeltaX) >= width) {
155             // If deltaX is too large, constrain to
156             // maxScrollDistanceWithResistance.
157             return (swipeDirection == Direction.RIGHT)
158                     ? maxSwipeDistanceWithResistance
159                     : -maxSwipeDistanceWithResistance;
160         } else {
161             // Otherwise, just attenuate deltaX.
162             return maxSwipeDistanceWithResistance
163                     * (float) Math.sin((moveDeltaX / width) * (Math.PI / 2));
164         }
165     }
166 }