Implementing an unfold animation using CannedAnimationController

Bug: 297057373
Flag: aconfig enable_unfold_state_animation DISABLED
Test: Manual
Change-Id: Iaa352133ebf06ab74b9ba4b8c4d3523f27666089
This commit is contained in:
Sunny Goyal
2024-01-18 11:49:37 -08:00
parent a3e2c59716
commit 2c206400a2
7 changed files with 451 additions and 7 deletions
+7
View File
@@ -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"
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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
//
@@ -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"
}
}
@@ -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<TransitionProgressListener> = 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)
}
}
}
}
@@ -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<RestoreInfo>
) {
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<RestoreInfo>
) {
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<RestoreInfo>()
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) } }
}
}