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