• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
19 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION;
20 import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY;
21 
22 import android.annotation.Nullable;
23 import android.os.Trace;
24 import android.util.FloatProperty;
25 import android.util.MathUtils;
26 import android.view.WindowManager;
27 
28 import androidx.core.view.OneShotPreDrawListener;
29 
30 import com.android.launcher3.DeviceProfile;
31 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
32 import com.android.launcher3.Hotseat;
33 import com.android.launcher3.Workspace;
34 import com.android.launcher3.uioverrides.QuickstepLauncher;
35 import com.android.launcher3.util.HorizontalInsettableView;
36 import com.android.quickstep.SystemUiProxy;
37 import com.android.quickstep.util.unfold.LauncherJankMonitorTransitionProgressListener;
38 import com.android.quickstep.util.unfold.PreemptiveUnfoldTransitionProgressProvider;
39 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
40 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
41 import com.android.systemui.unfold.dagger.UnfoldMain;
42 import com.android.systemui.unfold.updates.RotationChangeProvider;
43 import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider;
44 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
45 
46 /**
47  * Controls animations that are happening during unfolding foldable devices
48  */
49 public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeListener {
50 
51     // Percentage of the width of the quick search bar that will be reduced
52     // from the both sides of the bar when progress is 0
53     private static final float MAX_WIDTH_INSET_FRACTION = 0.04f;
54     private static final FloatProperty<Workspace<?>> WORKSPACE_SCALE_PROPERTY =
55             WORKSPACE_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION);
56     private static final FloatProperty<Hotseat> HOTSEAT_SCALE_PROPERTY =
57             HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION);
58 
59     private final QuickstepLauncher mLauncher;
60     private final ScopedUnfoldTransitionProgressProvider mProgressProvider;
61     private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider;
62     private final UnfoldMoveFromCenterHotseatAnimator mUnfoldMoveFromCenterHotseatAnimator;
63     private final UnfoldMoveFromCenterWorkspaceAnimator mUnfoldMoveFromCenterWorkspaceAnimator;
64     private final TransitionStatusProvider mExternalTransitionStatusProvider =
65             new TransitionStatusProvider();
66     private PreemptiveUnfoldTransitionProgressProvider mPreemptiveProgressProvider = null;
67     private Boolean mIsTablet = null;
68 
69     private static final String TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION =
70             "LauncherUnfoldAnimationController#waitingForTheNextFrame";
71 
72     @Nullable
73     private HorizontalInsettableView mQsbInsettable;
74 
LauncherUnfoldAnimationController( QuickstepLauncher launcher, WindowManager windowManager, UnfoldTransitionProgressProvider unfoldTransitionProgressProvider, @UnfoldMain RotationChangeProvider rotationChangeProvider)75     public LauncherUnfoldAnimationController(
76             QuickstepLauncher launcher,
77             WindowManager windowManager,
78             UnfoldTransitionProgressProvider unfoldTransitionProgressProvider,
79             @UnfoldMain RotationChangeProvider rotationChangeProvider) {
80         mLauncher = launcher;
81 
82         mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider(
83                 unfoldTransitionProgressProvider, launcher.getMainThreadHandler());
84         mPreemptiveProgressProvider.init();
85 
86         mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
87                 mPreemptiveProgressProvider);
88 
89         unfoldTransitionProgressProvider.addCallback(mExternalTransitionStatusProvider);
90         unfoldTransitionProgressProvider.addCallback(
91                 new LauncherJankMonitorTransitionProgressListener(launcher::getRootView));
92 
93         mUnfoldMoveFromCenterHotseatAnimator = new UnfoldMoveFromCenterHotseatAnimator(launcher,
94                 windowManager, rotationChangeProvider);
95         mUnfoldMoveFromCenterWorkspaceAnimator = new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
96                 windowManager, rotationChangeProvider);
97         mNaturalOrientationProgressProvider = new NaturalRotationUnfoldProgressProvider(launcher,
98                 rotationChangeProvider, mProgressProvider);
99         mNaturalOrientationProgressProvider.init();
100 
101         // Animated in all orientations
102         mProgressProvider.addCallback(mUnfoldMoveFromCenterWorkspaceAnimator);
103         mProgressProvider.addCallback(new LauncherScaleAnimationListener());
104 
105         // Animated only in natural orientation
106         mNaturalOrientationProgressProvider.addCallback(new QsbAnimationListener());
107         mNaturalOrientationProgressProvider.addCallback(mUnfoldMoveFromCenterHotseatAnimator);
108 
109         mLauncher.addOnDeviceProfileChangeListener(this);
110     }
111 
112     /**
113      * Called when launcher is resumed
114      */
onResume()115     public void onResume() {
116         Hotseat hotseat = mLauncher.getHotseat();
117         if (hotseat != null && hotseat.getQsb() instanceof HorizontalInsettableView) {
118             mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb();
119         }
120 
121         mProgressProvider.setReadyToHandleTransition(true);
122     }
123 
preemptivelyStartAnimationOnNextFrame()124     private void preemptivelyStartAnimationOnNextFrame() {
125         Trace.asyncTraceBegin(Trace.TRACE_TAG_APP,
126                 TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
127 
128         // Start the animation (and apply the transformations) in pre-draw listener to make sure
129         // that the views are laid out as some transformations depend on the view sizes and position
130         OneShotPreDrawListener.add(mLauncher.getWorkspace(),
131                 () -> {
132                     Trace.asyncTraceEnd(Trace.TRACE_TAG_APP,
133                             TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
134                     mPreemptiveProgressProvider.preemptivelyStartTransition(
135                             /* initialProgress= */ 0f);
136                 });
137     }
138 
139     /**
140      * Called when launcher activity is paused
141      */
onPause()142     public void onPause() {
143         mProgressProvider.setReadyToHandleTransition(false);
144         mQsbInsettable = null;
145     }
146 
147     /**
148      * Called when launcher activity is destroyed
149      */
onDestroy()150     public void onDestroy() {
151         mProgressProvider.destroy();
152         mNaturalOrientationProgressProvider.destroy();
153         mLauncher.removeOnDeviceProfileChangeListener(this);
154     }
155 
156     /**
157      * Called when launcher has finished binding its items
158      */
updateRegisteredViewsIfNeeded()159     public void updateRegisteredViewsIfNeeded() {
160         mUnfoldMoveFromCenterHotseatAnimator.updateRegisteredViewsIfNeeded();
161         mUnfoldMoveFromCenterWorkspaceAnimator.updateRegisteredViewsIfNeeded();
162     }
163 
164     @Override
onDeviceProfileChanged(DeviceProfile dp)165     public void onDeviceProfileChanged(DeviceProfile dp) {
166         if (mIsTablet != null && dp.isTablet != mIsTablet) {
167             // We should preemptively start the animation only if:
168             // - We changed to the unfolded screen
169             // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't
170             //   receive transition progress events from SystemUI later because there was no
171             //   IPC connection established (e.g. because of SystemUI crash)
172             // - SystemUI has not already sent unfold animation progress events. This might happen
173             //   if Launcher was not open during unfold, in this case we receive the configuration
174             //   change only after we went back to home screen and we don't want to start the
175             //   animation in this case.
176             if (dp.isTablet
177                     && SystemUiProxy.INSTANCE.get(mLauncher).isActive()
178                     && !mExternalTransitionStatusProvider.hasRun()) {
179                 // Preemptively start the unfold animation to make sure that we have drawn
180                 // the first frame of the animation before the screen gets unblocked
181                 preemptivelyStartAnimationOnNextFrame();
182             }
183 
184             if (!dp.isTablet) {
185                 mExternalTransitionStatusProvider.onFolded();
186             }
187         }
188 
189         mIsTablet = dp.isTablet;
190     }
191 
192     private class QsbAnimationListener implements TransitionProgressListener {
193 
194         @Override
onTransitionStarted()195         public void onTransitionStarted() {
196         }
197 
198         @Override
onTransitionFinished()199         public void onTransitionFinished() {
200             if (mQsbInsettable != null) {
201                 mQsbInsettable.setHorizontalInsets(0);
202             }
203         }
204 
205         @Override
onTransitionProgress(float progress)206         public void onTransitionProgress(float progress) {
207             if (mQsbInsettable != null) {
208                 float insetPercentage = (1 - progress) * MAX_WIDTH_INSET_FRACTION;
209                 mQsbInsettable.setHorizontalInsets(insetPercentage);
210             }
211         }
212     }
213 
214     private class LauncherScaleAnimationListener implements TransitionProgressListener {
215 
216         private static final float SCALE_LAUNCHER_FROM = 0.92f;
217 
218         @Override
onTransitionStarted()219         public void onTransitionStarted() {
220             mLauncher.getWorkspace().setPivotToScaleWithSelf(mLauncher.getHotseat());
221         }
222 
223         @Override
onTransitionFinished()224         public void onTransitionFinished() {
225             setScale(1);
226         }
227 
228         @Override
onTransitionProgress(float progress)229         public void onTransitionProgress(float progress) {
230             setScale(MathUtils.constrainedMap(SCALE_LAUNCHER_FROM, 1, 0, 1, progress));
231         }
232 
setScale(float value)233         private void setScale(float value) {
234             WORKSPACE_SCALE_PROPERTY.setValue(mLauncher.getWorkspace(), value);
235             HOTSEAT_SCALE_PROPERTY.setValue(mLauncher.getHotseat(), value);
236         }
237     }
238 
239     /**
240      * Class to track the current status of the external transition provider (the events are coming
241      * from the SystemUI side through IPC), it allows to check if the transition has already
242      * finished or currently running on the SystemUI side since last unfold.
243      */
244     private static class TransitionStatusProvider implements TransitionProgressListener {
245 
246         private boolean mHasRun = false;
247 
248         @Override
onTransitionStarted()249         public void onTransitionStarted() {
250             markAsRun();
251         }
252 
253         @Override
onTransitionProgress(float progress)254         public void onTransitionProgress(float progress) {
255             markAsRun();
256         }
257 
258         @Override
onTransitionFinished()259         public void onTransitionFinished() {
260             markAsRun();
261         }
262 
263         /**
264          * Called when the device is folded, so we can reset the status of the animation
265          */
onFolded()266         public void onFolded() {
267             mHasRun = false;
268         }
269 
270         /**
271          * Returns true if there was an animation already (or it is currently running) after
272          * unfolding the device
273          */
hasRun()274         public boolean hasRun() {
275             return mHasRun;
276         }
277 
markAsRun()278         private void markAsRun() {
279             mHasRun = true;
280         }
281     }
282 }
283