1 /*
2  * Copyright (C) 2022 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 package com.android.launcher3.taskbar;
17 
18 import static com.android.launcher3.anim.AnimatedFloat.VALUE;
19 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ObjectAnimator;
24 import android.animation.ValueAnimator;
25 
26 import androidx.annotation.Nullable;
27 import androidx.dynamicanimation.animation.SpringForce;
28 
29 import com.android.app.animation.Interpolators;
30 import com.android.launcher3.anim.AnimatedFloat;
31 import com.android.launcher3.anim.SpringAnimationBuilder;
32 import com.android.launcher3.util.DisplayController;
33 
34 import java.io.PrintWriter;
35 
36 /**
37  * Class responsible for translating the transient taskbar UI during a swipe gesture.
38  *
39  * The translation is controlled, in priority order:
40  * - animation to home
41  * - a spring animation
42  * - controlled by user
43  *
44  * The spring animation will play start once the user lets go or when user pauses to go to overview.
45  * When the user goes home, the stash animation will play.
46  */
47 public class TaskbarTranslationController implements TaskbarControllers.LoggableTaskbarController {
48 
49     private final TaskbarActivityContext mContext;
50     private TaskbarControllers mControllers;
51     private final AnimatedFloat mTranslationYForSwipe = new AnimatedFloat(
52             this::updateTranslationYForSwipe);
53 
54     private boolean mHasSprungOnceThisGesture;
55     private @Nullable ValueAnimator mSpringBounce;
56     private boolean mGestureInProgress;
57     private boolean mGestureEnded;
58     private boolean mAnimationToHomeRunning;
59 
60     private final boolean mIsTransientTaskbar;
61 
62     private final TransitionCallback mCallback;
63 
TaskbarTranslationController(TaskbarActivityContext context)64     public TaskbarTranslationController(TaskbarActivityContext context) {
65         mContext = context;
66         mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
67         mCallback = new TransitionCallback();
68     }
69 
70     /**
71      * Initialization method.
72      */
init(TaskbarControllers controllers)73     public void init(TaskbarControllers controllers) {
74         mControllers = controllers;
75     }
76 
77     /**
78      * Called to cancel any existing animations.
79      */
cancelSpringIfExists()80     public void cancelSpringIfExists() {
81         if (mSpringBounce != null) {
82             mSpringBounce.cancel();
83             mSpringBounce = null;
84         }
85     }
86 
updateTranslationYForSwipe()87     private void updateTranslationYForSwipe() {
88         if (!mIsTransientTaskbar) {
89             return;
90         }
91 
92         float transY = mTranslationYForSwipe.value;
93         mControllers.stashedHandleViewController.setTranslationYForSwipe(transY);
94         mControllers.taskbarViewController.setTranslationYForSwipe(transY);
95         mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
96         mControllers.bubbleControllers.ifPresent(controllers -> {
97             controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
98             controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
99         });
100     }
101 
102     /**
103      * Starts a spring aniamtion to set the views back to the resting state.
104      */
startSpring()105     public void startSpring() {
106         if (mHasSprungOnceThisGesture || mAnimationToHomeRunning) {
107             return;
108         }
109         mSpringBounce = new SpringAnimationBuilder(mContext)
110                 .setStartValue(mTranslationYForSwipe.value)
111                 .setEndValue(0)
112                 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
113                 .setStiffness(SpringForce.STIFFNESS_LOW)
114                 .build(mTranslationYForSwipe, VALUE);
115         mSpringBounce.addListener(forEndCallback(() -> {
116             if (!mGestureEnded) {
117                 return;
118             }
119             reset();
120             if (mControllers.taskbarStashController.isInApp()
121                     && mControllers.taskbarStashController.isTaskbarVisibleAndNotStashing()) {
122                 mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
123             }
124         }));
125         mSpringBounce.start();
126         mHasSprungOnceThisGesture = true;
127     }
128 
reset()129     private void reset() {
130         mGestureEnded = false;
131         mHasSprungOnceThisGesture = false;
132     }
133 
134     /**
135      * Returns a callback to help monitor the swipe gesture.
136      */
getTransitionCallback()137     public TransitionCallback getTransitionCallback() {
138         return mCallback;
139     }
140 
141     /**
142      * Returns true if we will animate to zero before the input duration.
143      */
willAnimateToZeroBefore(long duration)144     public boolean willAnimateToZeroBefore(long duration) {
145         if (mSpringBounce != null && mSpringBounce.isRunning()) {
146             long springDuration = mSpringBounce.getDuration();
147             long current = mSpringBounce.getCurrentPlayTime();
148             return (springDuration - current < duration);
149         }
150         if (mTranslationYForSwipe.isAnimatingToValue(0)) {
151             return mTranslationYForSwipe.getRemainingTime() < duration;
152         }
153         return false;
154     }
155 
156     /**
157      * Returns an animation to reset the taskbar translation to {@code 0}.
158      */
createAnimToResetTranslation(long duration)159     public ValueAnimator createAnimToResetTranslation(long duration) {
160         if (mGestureInProgress) {
161             // Return an empty animator as the translation will reset itself after gesture ends.
162             return ValueAnimator.ofFloat(0).setDuration(duration);
163         }
164 
165         ObjectAnimator animator = mTranslationYForSwipe.animateToValue(0);
166         animator.setInterpolator(Interpolators.LINEAR);
167         animator.setDuration(duration);
168         animator.addListener(new AnimatorListenerAdapter() {
169             @Override
170             public void onAnimationStart(Animator animation) {
171                 cancelSpringIfExists();
172                 reset();
173                 mAnimationToHomeRunning = true;
174             }
175 
176             @Override
177             public void onAnimationEnd(Animator animation) {
178                 mAnimationToHomeRunning = false;
179                 reset();
180             }
181         });
182         return animator;
183     }
184 
185     /**
186      * Helper class to communicate to/from  the input consumer.
187      */
188     public class TransitionCallback {
189 
190         /**
191          * Clears any existing animations so that user
192          * can take control over the movement of the taskbaer.
193          */
onActionDown()194         public void onActionDown() {
195             if (mAnimationToHomeRunning) {
196                 mTranslationYForSwipe.cancelAnimation();
197             }
198             mAnimationToHomeRunning = false;
199             cancelSpringIfExists();
200             reset();
201             mGestureInProgress = true;
202         }
203         /**
204          * Called when there is movement to move the taskbar.
205          */
onActionMove(float dY)206         public void onActionMove(float dY) {
207             if (mAnimationToHomeRunning
208                     || (mHasSprungOnceThisGesture && !mGestureEnded)) {
209                 return;
210             }
211 
212             mTranslationYForSwipe.updateValue(dY);
213         }
214 
215         /**
216          * Called when swipe gesture has ended.
217          */
onActionEnd()218         public void onActionEnd() {
219             if (mHasSprungOnceThisGesture) {
220                 reset();
221             } else {
222                 mGestureEnded = true;
223                 startSpring();
224             }
225             mGestureInProgress = false;
226         }
227     }
228 
229     @Override
dumpLogs(String prefix, PrintWriter pw)230     public void dumpLogs(String prefix, PrintWriter pw) {
231         pw.println(prefix + "TaskbarTranslationController:");
232 
233         pw.println(prefix + "\tmTranslationYForSwipe=" + mTranslationYForSwipe.value);
234         pw.println(prefix + "\tmHasSprungOnceThisGesture=" + mHasSprungOnceThisGesture);
235         pw.println(prefix + "\tmAnimationToHomeRunning=" + mAnimationToHomeRunning);
236         pw.println(prefix + "\tmGestureEnded=" + mGestureEnded);
237         pw.println(prefix + "\tmSpringBounce is running=" + (mSpringBounce != null
238                 && mSpringBounce.isRunning()));
239     }
240 }
241 
242