1 /*
2  * Copyright (C) 2020 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.quickstep.util;
17 
18 import static com.android.app.animation.Interpolators.DECELERATE;
19 import static com.android.app.animation.Interpolators.LINEAR;
20 import static com.android.launcher3.Flags.enableGridOnlyOverview;
21 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
22 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
23 
24 import android.animation.AnimatorSet;
25 import android.animation.TimeInterpolator;
26 import android.content.Context;
27 import android.graphics.Matrix;
28 import android.graphics.PointF;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.util.FloatProperty;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.launcher3.DeviceProfile;
36 import com.android.launcher3.Launcher;
37 import com.android.launcher3.LauncherState;
38 import com.android.launcher3.Utilities;
39 import com.android.launcher3.anim.AnimatorPlaybackController;
40 import com.android.launcher3.anim.PendingAnimation;
41 import com.android.launcher3.statemanager.StateManager;
42 import com.android.launcher3.statemanager.StatefulActivity;
43 import com.android.launcher3.states.StateAnimationConfig;
44 import com.android.launcher3.touch.AllAppsSwipeController;
45 import com.android.quickstep.DeviceConfigWrapper;
46 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
47 import com.android.quickstep.views.RecentsView;
48 import com.android.quickstep.views.RecentsViewContainer;
49 
50 /**
51  * Controls an animation that can go beyond progress = 1, at which point resistance should be
52  * applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that
53  * runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but
54  * starts applying resistance as well.
55  */
56 public class AnimatorControllerWithResistance {
57 
58     private enum RecentsResistanceParams {
59         FROM_APP(0.75f, 0.5f, 1f, false),
60         FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false),
61         FROM_APP_TABLET(1f, 0.7f, 1f, true),
62         FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true),
63         FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false),
64         FROM_OVERVIEW(1f, 0.75f, 0.5f, false);
65 
RecentsResistanceParams(float scaleStartResist, float scaleMaxResist, float translationFactor, boolean stopScalingAtTop)66         RecentsResistanceParams(float scaleStartResist, float scaleMaxResist,
67                 float translationFactor, boolean stopScalingAtTop) {
68             this.scaleStartResist = scaleStartResist;
69             this.scaleMaxResist = scaleMaxResist;
70             this.translationFactor = translationFactor;
71             this.stopScalingAtTop = stopScalingAtTop;
72         }
73 
74         /**
75          * Start slowing down the rate of scaling down when recents view is smaller than this scale.
76          */
77         public final float scaleStartResist;
78 
79         /**
80          * Recents view will reach this scale at the very end of the drag.
81          */
82         public final float scaleMaxResist;
83 
84         /**
85          * How much translation to apply to RecentsView when the drag reaches the top of the screen,
86          * where 0 will keep it centered and 1 will have it barely touch the top of the screen.
87          */
88         public final float translationFactor;
89 
90         /**
91          * Whether to end scaling effect when the scaled down version of TaskView's top reaches the
92          * non-scaled version of TaskView's top.
93          */
94         public final boolean stopScalingAtTop;
95     }
96 
97     private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DECELERATE;
98     private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
99 
100     private static final Rect TEMP_RECT = new Rect();
101 
102     private final AnimatorPlaybackController mNormalController;
103     private final AnimatorPlaybackController mResistanceController;
104 
105     // Initialize to -1 so the first 0 gets applied.
106     private float mLastNormalProgress = -1;
107     private float mLastResistProgress;
108 
AnimatorControllerWithResistance(AnimatorPlaybackController normalController, AnimatorPlaybackController resistanceController)109     public AnimatorControllerWithResistance(AnimatorPlaybackController normalController,
110             AnimatorPlaybackController resistanceController) {
111         mNormalController = normalController;
112         mResistanceController = resistanceController;
113     }
114 
getNormalController()115     public AnimatorPlaybackController getNormalController() {
116         return mNormalController;
117     }
118 
119     /**
120      * Applies the current progress of the animation.
121      * @param progress From 0 to maxProgress, where 1 is the target we are animating towards.
122      * @param maxProgress > 1, this is where the resistance will be applied.
123      */
setProgress(float progress, float maxProgress)124     public void setProgress(float progress, float maxProgress) {
125         float normalProgress = Utilities.boundToRange(progress, 0, 1);
126         if (normalProgress != mLastNormalProgress) {
127             mLastNormalProgress = normalProgress;
128             mNormalController.setPlayFraction(normalProgress);
129         }
130         if (maxProgress <= 1) {
131             return;
132         }
133         float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress);
134         if (resistProgress != mLastResistProgress) {
135             mLastResistProgress = resistProgress;
136             mResistanceController.setPlayFraction(resistProgress);
137         }
138     }
139 
140     /**
141      * Applies resistance to recents when swiping up past its target position.
142      * @param normalController The controller to run from 0 to 1 before this resistance applies.
143      * @param context Used to compute start and end values.
144      * @param recentsOrientedState Used to compute start and end values.
145      * @param dp Used to compute start and end values.
146      * @param scaleTarget The target for the scaleProperty.
147      * @param scaleProperty Animate the value to change the scale of the window/recents view.
148      * @param translationTarget The target for the translationProperty.
149      * @param translationProperty Animate the value to change the translation of the recents view.
150      */
createForRecents( AnimatorPlaybackController normalController, Context context, RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty)151     public static <SCALE, TRANSLATION> AnimatorControllerWithResistance createForRecents(
152             AnimatorPlaybackController normalController, Context context,
153             RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
154             FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
155             FloatProperty<TRANSLATION> translationProperty) {
156 
157         RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget,
158                 scaleProperty, translationTarget, translationProperty);
159         PendingAnimation resistAnim = createRecentsResistanceAnim(params);
160 
161         // Apply All Apps animation during the resistance animation.
162         if (recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()) {
163             RecentsViewContainer container =
164                     recentsOrientedState.getContainerInterface().getCreatedContainer();
165             if (container != null) {
166                 RecentsView recentsView = container.getOverviewPanel();
167                 StateManager<LauncherState, StatefulActivity<LauncherState>> stateManager =
168                         recentsView.getStateManager();
169                 if (stateManager.isInStableState(LauncherState.BACKGROUND_APP)
170                         && stateManager.isInTransition()) {
171 
172                     // Calculate the resistance progress threshold where All Apps will trigger.
173                     float threshold = getAllAppsThreshold(context, recentsOrientedState, dp);
174 
175                     StateAnimationConfig config = new StateAnimationConfig();
176                     AllAppsSwipeController.applyOverviewToAllAppsAnimConfig(dp, config, threshold);
177                     AnimatorSet allAppsAnimator = stateManager.createAnimationToNewWorkspace(
178                             LauncherState.ALL_APPS, config).getTarget();
179                     resistAnim.add(allAppsAnimator);
180                 }
181             }
182         }
183 
184         AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
185         return new AnimatorControllerWithResistance(normalController, resistanceController);
186     }
187 
getAllAppsThreshold(Context context, RecentsOrientedState recentsOrientedState, DeviceProfile dp)188     private static float getAllAppsThreshold(Context context,
189             RecentsOrientedState recentsOrientedState, DeviceProfile dp) {
190         int transitionDragLength =
191                 recentsOrientedState.getContainerInterface().getSwipeUpDestinationAndLength(
192                         dp, context, TEMP_RECT,
193                         recentsOrientedState.getOrientationHandler());
194         float dragLengthFactor = (float) dp.heightPx / transitionDragLength;
195         // -1s are because 0-1 is reserved for the normal transition.
196         float threshold = DeviceConfigWrapper.get().getAllAppsOverviewThreshold() / 100f;
197         return (threshold - 1) / (dragLengthFactor - 1);
198     }
199 
200     /**
201      * Creates the resistance animation for {@link #createForRecents}, or can be used separately
202      * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
203      */
createRecentsResistanceAnim( RecentsParams<SCALE, TRANSLATION> params)204     public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
205             RecentsParams<SCALE, TRANSLATION> params) {
206         Rect startRect = new Rect();
207         RecentsPagedOrientationHandler orientationHandler = params.recentsOrientedState
208                 .getOrientationHandler();
209         params.recentsOrientedState.getContainerInterface()
210                 .calculateTaskSize(params.context, params.dp, startRect, orientationHandler);
211         long distanceToCover = startRect.bottom;
212         PendingAnimation resistAnim = params.resistAnim != null
213                 ? params.resistAnim
214                 : new PendingAnimation(distanceToCover * 2);
215 
216         PointF pivot = new PointF();
217         float fullscreenScale = params.recentsOrientedState.getFullScreenScaleAndPivot(
218                 startRect, params.dp, pivot);
219 
220         // Compute where the task view would be based on the end scale.
221         RectF endRectF = new RectF(startRect);
222         Matrix temp = new Matrix();
223         temp.setScale(params.resistanceParams.scaleMaxResist,
224                 params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
225         temp.mapRect(endRectF);
226         // Translate such that the task view touches the top of the screen when drag does.
227         float endTranslation = endRectF.top
228                 * orientationHandler.getSecondaryTranslationDirectionFactor()
229                 * params.resistanceParams.translationFactor;
230         resistAnim.addFloat(params.translationTarget, params.translationProperty,
231                 params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
232 
233         float prevScaleRate = (fullscreenScale - params.startScale)
234                 / (params.dp.heightPx - startRect.bottom);
235         // This is what the scale would be at the end of the drag if we didn't apply resistance.
236         float endScale = params.startScale - prevScaleRate * distanceToCover;
237         // Create an interpolator that resists the scale so the scale doesn't get smaller than
238         // RECENTS_SCALE_MAX_RESIST.
239         float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
240                 params.startScale, endScale);
241         float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
242                 params.startScale, endScale);
243         float stopResist =
244                 params.resistanceParams.stopScalingAtTop ? 1f - startRect.top / endRectF.top : 1f;
245         final TimeInterpolator scaleInterpolator = t -> {
246             if (t <= startResist) {
247                 return t;
248             }
249             if (t >= stopResist) {
250                 return maxResist;
251             }
252             float resistProgress = Utilities.getProgress(t, startResist, stopResist);
253             resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
254             return startResist + resistProgress * (maxResist - startResist);
255         };
256         resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale,
257                 scaleInterpolator);
258 
259         return resistAnim;
260     }
261 
262     /**
263      * Helper method to update or create a PendingAnimation suitable for animating
264      * a RecentsView interaction that started from the overview state.
265      */
createRecentsResistanceFromOverviewAnim( Launcher launcher, @Nullable PendingAnimation resistanceAnim)266     public static PendingAnimation createRecentsResistanceFromOverviewAnim(
267             Launcher launcher, @Nullable PendingAnimation resistanceAnim) {
268         RecentsView recentsView = launcher.getOverviewPanel();
269         RecentsParams params = new RecentsParams(launcher, recentsView.getPagedViewOrientedState(),
270                 launcher.getDeviceProfile(), recentsView, RECENTS_SCALE_PROPERTY, recentsView,
271                 TASK_SECONDARY_TRANSLATION)
272                 .setResistAnim(resistanceAnim)
273                 .setResistanceParams(RecentsResistanceParams.FROM_OVERVIEW)
274                 .setStartScale(recentsView.getScaleX());
275         return createRecentsResistanceAnim(params);
276     }
277 
278     /**
279      * Params to compute resistance when scaling/translating recents.
280      */
281     private static class RecentsParams<SCALE, TRANSLATION> {
282         // These are all required and can't have default values, hence are final.
283         public final Context context;
284         public final RecentsOrientedState recentsOrientedState;
285         public final DeviceProfile dp;
286         public final SCALE scaleTarget;
287         public final FloatProperty<SCALE> scaleProperty;
288         public final TRANSLATION translationTarget;
289         public final FloatProperty<TRANSLATION> translationProperty;
290 
291         // These are not required, or can have a default value that is generally correct.
292         @Nullable public PendingAnimation resistAnim = null;
293         public RecentsResistanceParams resistanceParams;
294         public float startScale = 1f;
295         public float startTranslation = 0f;
296 
RecentsParams(Context context, RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty)297         private RecentsParams(Context context, RecentsOrientedState recentsOrientedState,
298                 DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty,
299                 TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty) {
300             this.context = context;
301             this.recentsOrientedState = recentsOrientedState;
302             this.dp = dp;
303             this.scaleTarget = scaleTarget;
304             this.scaleProperty = scaleProperty;
305             this.translationTarget = translationTarget;
306             this.translationProperty = translationProperty;
307             if (dp.isTablet) {
308                 resistanceParams =
309                         recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()
310                                 ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET
311                                 : enableGridOnlyOverview()
312                                         ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY
313                                         : RecentsResistanceParams.FROM_APP_TABLET;
314             } else {
315                 resistanceParams =
316                         recentsOrientedState.getContainerInterface().allowAllAppsFromOverview()
317                                 ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS
318                                 : RecentsResistanceParams.FROM_APP;
319             }
320         }
321 
setResistAnim(PendingAnimation resistAnim)322         private RecentsParams setResistAnim(PendingAnimation resistAnim) {
323             this.resistAnim = resistAnim;
324             return this;
325         }
326 
setResistanceParams(RecentsResistanceParams resistanceParams)327         private RecentsParams setResistanceParams(RecentsResistanceParams resistanceParams) {
328             this.resistanceParams = resistanceParams;
329             return this;
330         }
331 
setStartScale(float startScale)332         private RecentsParams setStartScale(float startScale) {
333             this.startScale = startScale;
334             return this;
335         }
336 
setStartTranslation(float startTranslation)337         private RecentsParams setStartTranslation(float startTranslation) {
338             this.startTranslation = startTranslation;
339             return this;
340         }
341     }
342 }
343