diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index f34105a146..570371111b 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -127,3 +127,10 @@ flag { description: "Enables asnc inflation of workspace icons" bug: "318539160" } + +flag { + name: "enable_unfold_state_animation" + namespace: "launcher" + description: "Tie unfold animation with state animation" + bug: "297057373" +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 33641a4bda..7c7c426d19 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -22,6 +22,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate; @@ -405,8 +406,12 @@ public class TaskbarManager { */ private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity( StatefulActivity activity) { - if (activity instanceof QuickstepLauncher) { - return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider(); + if (!enableUnfoldStateAnimation()) { + if (activity instanceof QuickstepLauncher ql) { + return ql.getUnfoldTransitionProgressProvider(); + } + } else { + return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider(); } return null; } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index f0ab08c921..b1ea0deb60 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEAS import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; import static com.android.app.animation.Interpolators.EMPHASIZED; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE; import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON; @@ -170,6 +171,8 @@ import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SplitToWorkspaceController; import com.android.quickstep.util.SplitWithKeyboardShortcutController; import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.util.unfold.LauncherUnfoldTransitionController; +import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; @@ -955,9 +958,17 @@ public class QuickstepLauncher extends Launcher { } private void initUnfoldTransitionProgressProvider() { - final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig(); - if (config.isEnabled()) { - initRemotelyCalculatedUnfoldAnimation(config); + if (!enableUnfoldStateAnimation()) { + final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig(); + if (config.isEnabled()) { + initRemotelyCalculatedUnfoldAnimation(config); + } + } else { + ProxyUnfoldTransitionProvider provider = + SystemUiProxy.INSTANCE.get(this).getUnfoldTransitionProvider(); + if (provider != null) { + new LauncherUnfoldTransitionController(this, provider); + } } } diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 723af43468..56a4024c7f 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -17,6 +17,7 @@ package com.android.quickstep; import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition; @@ -67,6 +68,7 @@ import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; @@ -74,6 +76,7 @@ import com.android.systemui.shared.system.RecentsAnimationListener; import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController; import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController; import com.android.systemui.shared.system.smartspace.SmartspaceState; +import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig; import com.android.systemui.unfold.progress.IUnfoldAnimation; import com.android.systemui.unfold.progress.IUnfoldTransitionListener; import com.android.wm.shell.back.IBackAnimation; @@ -177,7 +180,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { */ private final PendingIntent mRecentsPendingIntent; - public SystemUiProxy(Context context) { + @Nullable + private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider; + + private SystemUiProxy(Context context) { mContext = context; mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync); final Intent baseIntent = new Intent().setPackage(mContext.getPackageName()); @@ -187,6 +193,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { mRecentsPendingIntent = PendingIntent.getActivity(mContext, 0, baseIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT | Intent.FILL_IN_COMPONENT, options.toBundle()); + + mUnfoldTransitionProvider = + (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled()) + ? new ProxyUnfoldTransitionProvider() : null; } @Override @@ -251,7 +261,7 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { mRecentTasks = recentTasks; mBackAnimation = backAnimation; mDesktopMode = desktopMode; - mUnfoldAnimation = unfoldAnimation; + mUnfoldAnimation = enableUnfoldStateAnimation() ? null : unfoldAnimation; mDragAndDrop = dragAndDrop; linkToDeath(); // re-attach the listeners once missing due to setProxy has not been initialized yet. @@ -272,6 +282,19 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { setAssistantOverridesRequested( AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes()); mStateChangeCallbacks.forEach(Runnable::run); + + if (mUnfoldTransitionProvider != null) { + if (unfoldAnimation != null) { + try { + unfoldAnimation.setListener(mUnfoldTransitionProvider); + mUnfoldTransitionProvider.setActive(true); + } catch (RemoteException e) { + // Ignore + } + } else { + mUnfoldTransitionProvider.setActive(false); + } + } } /** @@ -1451,6 +1474,11 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { } } + @Nullable + public ProxyUnfoldTransitionProvider getUnfoldTransitionProvider() { + return mUnfoldTransitionProvider; + } + // // Recents // diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt new file mode 100644 index 0000000000..54d317d564 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import android.app.Activity +import android.os.Trace +import android.view.Surface +import com.android.launcher3.Alarm +import com.android.launcher3.DeviceProfile +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener +import com.android.launcher3.Launcher +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener + +/** Controls animations that are happening during unfolding foldable devices */ +class LauncherUnfoldTransitionController( + private val launcher: Launcher, + private val progressProvider: ProxyUnfoldTransitionProvider +) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener { + + private var isTablet: Boolean? = null + private var hasUnfoldTransitionStarted = false + private val timeoutAlarm = + Alarm().apply { + setOnAlarmListener { + onTransitionFinished() + Trace.endAsyncSection("$TAG#startedPreemptively", 0) + } + } + + init { + launcher.addOnDeviceProfileChangeListener(this) + launcher.registerActivityLifecycleCallbacks(this) + } + + override fun onActivityPaused(activity: Activity) { + progressProvider.removeCallback(this) + } + + override fun onActivityResumed(activity: Activity) { + progressProvider.addCallback(this) + } + + override fun onDeviceProfileChanged(dp: DeviceProfile) { + if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { + return + } + + if (isTablet != null && dp.isTablet != isTablet) { + // We should preemptively start the animation only if: + // - We changed to the unfolded screen + // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't + // receive transition progress events from SystemUI later because there was no + // IPC connection established (e.g. because of SystemUI crash) + // - SystemUI has not already sent unfold animation progress events. This might happen + // if Launcher was not open during unfold, in this case we receive the configuration + // change only after we went back to home screen and we don't want to start the + // animation in this case. + if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) { + // Preemptively start the unfold animation to make sure that we have drawn + // the first frame of the animation before the screen gets unblocked + onTransitionStarted() + Trace.beginAsyncSection("$TAG#startedPreemptively", 0) + timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS) + } + if (!dp.isTablet) { + // Reset unfold transition status when folded + hasUnfoldTransitionStarted = false + } + } + + isTablet = dp.isTablet + } + + override fun onTransitionStarted() { + hasUnfoldTransitionStarted = true + launcher.animationCoordinator.setAnimation( + provider = this, + factory = this::onPrepareUnfoldAnimation, + duration = + 1000L // The expected duration for the animation. Then only comes to play if we have + // to run the animation ourselves in case sysui misses the end signal + ) + timeoutAlarm.cancelAlarm() + } + + override fun onTransitionProgress(progress: Float) { + hasUnfoldTransitionStarted = true + launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress) + } + + override fun onTransitionFinished() { + // Run the animation to end the animation in case it is not already at end progress. It + // will scale the duration to the remaining progress + launcher.animationCoordinator.getPlaybackController(this)?.start() + timeoutAlarm.cancelAlarm() + } + + private fun onPrepareUnfoldAnimation(anim: PendingAnimation) { + val dp = launcher.deviceProfile + val rotation = dp.displayInfo.rotation + val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 + UnfoldAnimationBuilder.buildUnfoldAnimation( + launcher, + isVertical, + dp.displayInfo.currentSize, + anim + ) + } + + companion object { + private const val TAG = "LauncherUnfoldTransitionController" + } +} diff --git a/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt new file mode 100644 index 0000000000..83c7f72f21 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import androidx.annotation.AnyThread +import androidx.annotation.FloatRange +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.progress.IUnfoldTransitionListener +import com.android.systemui.unfold.progress.UnfoldRemoteFilter + +/** Receives unfold events from remote senders (System UI). */ +class ProxyUnfoldTransitionProvider : + UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() { + + private val listeners: MutableSet = mutableSetOf() + private val delegate = UnfoldRemoteFilter(ProcessedProgressListener()) + + private var transitionStarted = false + var isActive = false + set(value) { + field = value + if (!value) { + // Finish any active transition + onTransitionFinished() + } + } + + @AnyThread + override fun onTransitionStarted() { + MAIN_EXECUTOR.execute(delegate::onTransitionStarted) + } + + @AnyThread + override fun onTransitionProgress(progress: Float) { + MAIN_EXECUTOR.execute { delegate.onTransitionProgress(progress) } + } + + @AnyThread + override fun onTransitionFinished() { + MAIN_EXECUTOR.execute(delegate::onTransitionFinished) + } + + override fun addCallback(listener: TransitionProgressListener) { + listeners += listener + if (transitionStarted) { + // Update the listener in case there was is an active transition + listener.onTransitionStarted() + } + } + + override fun removeCallback(listener: TransitionProgressListener) { + listeners -= listener + if (transitionStarted) { + // Finish the transition if it was already running + listener.onTransitionFinished() + } + } + + override fun destroy() { + listeners.clear() + } + + private inner class ProcessedProgressListener : TransitionProgressListener { + override fun onTransitionStarted() { + if (!transitionStarted) { + transitionStarted = true + listeners.forEach(TransitionProgressListener::onTransitionStarted) + } + } + + override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { + listeners.forEach { it.onTransitionProgress(progress) } + } + + override fun onTransitionFinished() { + if (transitionStarted) { + transitionStarted = false + listeners.forEach(TransitionProgressListener::onTransitionFinished) + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt new file mode 100644 index 0000000000..d2c4728362 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import android.graphics.Point +import android.view.ViewGroup +import com.android.app.animation.Interpolators.LINEAR +import com.android.app.animation.Interpolators.clampToProgress +import com.android.launcher3.CellLayout +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY +import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y +import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY +import com.android.launcher3.Workspace +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.util.HorizontalInsettableView + +private typealias ViewGroupAction = (ViewGroup, Boolean) -> Unit + +object UnfoldAnimationBuilder { + + private val CLIP_CHILDREN: ViewGroupAction = ViewGroup::setClipChildren + private val CLIP_TO_PADDING: ViewGroupAction = ViewGroup::setClipToPadding + + data class RestoreInfo(val action: ViewGroupAction, var target: ViewGroup, var value: Boolean) + + // Percentage of the width of the quick search bar that will be reduced + // from the both sides of the bar when progress is 0 + private const val MAX_WIDTH_INSET_FRACTION = 0.04f + + // Scale factor for the whole workspace and hotseat + private const val SCALE_LAUNCHER_FROM = 0.92f + + // Translation factor for all the items on the homescreen + private const val TRANSLATION_PERCENTAGE = 0.08f + + private fun setClipChildren( + target: ViewGroup, + value: Boolean, + restoreList: MutableList + ) { + val originalValue = target.clipChildren + if (originalValue != value) { + target.clipChildren = value + restoreList.add(RestoreInfo(CLIP_CHILDREN, target, originalValue)) + } + } + + private fun setClipToPadding( + target: ViewGroup, + value: Boolean, + restoreList: MutableList + ) { + val originalValue = target.clipToPadding + if (originalValue != value) { + target.clipToPadding = value + restoreList.add(RestoreInfo(CLIP_TO_PADDING, target, originalValue)) + } + } + + private fun addChildrenAnimation( + itemsContainer: ViewGroup, + isVerticalFold: Boolean, + screenSize: Point, + anim: PendingAnimation + ) { + val tempLocation = IntArray(2) + for (i in 0 until itemsContainer.childCount) { + val child = itemsContainer.getChildAt(i) + + child.getLocationOnScreen(tempLocation) + if (isVerticalFold) { + val viewCenterX = tempLocation[0] + child.width / 2 + val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX + anim.addFloat( + child, + VIEW_TRANSLATE_X, + distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE, + 0f, + LINEAR + ) + } else { + val viewCenterY = tempLocation[1] + child.height / 2 + val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY + anim.addFloat( + child, + VIEW_TRANSLATE_Y, + distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE, + 0f, + LINEAR + ) + } + } + } + + /** + * Builds an animation for the unfold experience and adds it to the provided PendingAnimation + */ + fun buildUnfoldAnimation( + launcher: Launcher, + isVerticalFold: Boolean, + screenSize: Point, + anim: PendingAnimation + ) { + val restoreList = ArrayList() + val registerViews: (CellLayout) -> Unit = { cellLayout -> + setClipChildren(cellLayout, false, restoreList) + setClipToPadding(cellLayout, false, restoreList) + addChildrenAnimation(cellLayout.shortcutsAndWidgets, isVerticalFold, screenSize, anim) + } + + val workspace: Workspace<*> = launcher.workspace + val hotseat = launcher.hotseat + + // Animation icons from workspace for all orientations + workspace.forEachVisiblePage { registerViews(it as CellLayout) } + setClipChildren(workspace, false, restoreList) + setClipToPadding(workspace, true, restoreList) + + // Workspace scale + launcher.workspace.setPivotToScaleWithSelf(launcher.hotseat) + val interpolator = clampToProgress(LINEAR, 0f, 1f) + anim.addFloat( + workspace, + WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION], + SCALE_LAUNCHER_FROM, + 1f, + interpolator + ) + anim.addFloat( + hotseat, + HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION], + SCALE_LAUNCHER_FROM, + 1f, + interpolator + ) + + if (isVerticalFold) { + if (hotseat.qsb is HorizontalInsettableView) { + anim.addFloat( + hotseat.qsb as HorizontalInsettableView, + HorizontalInsettableView.HORIZONTAL_INSETS, + MAX_WIDTH_INSET_FRACTION, + 0f, + LINEAR + ) + } + registerViews(hotseat) + } + anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } } + } +}