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