diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java index 4218b94655..2f3068c8fd 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java @@ -39,7 +39,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.statemanager.StateManager.StateHandler; import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.quickstep.util.BaseDepthController; +import com.android.quickstep.util.BaseDepthControllerImpl; import java.io.PrintWriter; import java.util.function.Consumer; @@ -51,7 +51,7 @@ import app.lawnchair.preferences2.PreferenceManager2; /** * Controls blur and wallpaper zoom, for the Launcher surface only. */ -public class DepthController extends BaseDepthController implements StateHandler, +public class DepthController extends BaseDepthControllerImpl implements StateHandler, BaseActivity.MultiWindowModeChangedListener { @VisibleForTesting final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw; diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 1609d6503d..2ba3a3e532 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -1335,6 +1335,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, out.add(new RecentsViewStateController(this)); } + @Override public DepthController getDepthController() { return mDepthController; } diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthControllerImpl.java similarity index 90% rename from quickstep/src/com/android/quickstep/util/BaseDepthController.java rename to quickstep/src/com/android/quickstep/util/BaseDepthControllerImpl.java index 9d9aaf1c36..be397a471c 100644 --- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java +++ b/quickstep/src/com/android/quickstep/util/BaseDepthControllerImpl.java @@ -43,6 +43,8 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; +import com.android.launcher3.statehandlers.BaseDepthController; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.MultiPropertyFactory; @@ -52,20 +54,21 @@ import com.android.systemui.shared.system.BlurUtils; /** * Utility class for applying depth effect */ -public class BaseDepthController { +public class BaseDepthControllerImpl implements BaseDepthController { public static final float DEPTH_0_PERCENT = 0f; public static final float DEPTH_60_PERCENT = 0.6f; public static final float DEPTH_70_PERCENT = 0.7f; - private static final FloatProperty DEPTH = - new FloatProperty("depth") { + // Includes both Wallpaper Zoom and Blur + private static final FloatProperty DEPTH = + new FloatProperty("depth") { @Override - public void setValue(BaseDepthController depthController, float depth) { + public void setValue(BaseDepthControllerImpl depthController, float depth) { depthController.setDepth(depth); } @Override - public Float get(BaseDepthController depthController) { + public Float get(BaseDepthControllerImpl depthController) { return depthController.mDepth; } }; @@ -74,6 +77,9 @@ public class BaseDepthController { private static final int DEPTH_INDEX_WIDGET = 1; private static final int DEPTH_INDEX_COUNT = 2; + private static final int ZOOM_INDEX_FOLDER = 0; + private static final int ZOOM_INDEX_COUNT = 1; + // b/291401432 private static final String TAG = "BaseDepthController"; @@ -82,6 +88,8 @@ public class BaseDepthController { public final MultiProperty stateDepth; /** Property to set the depth for widget picker. */ public final MultiProperty widgetDepth; + /** Property to set just the wallpaper zoom for folder expansion. */ + public final MultiProperty folderZoom; /** * Blur radius when completely zoomed out, in pixels. @@ -96,6 +104,8 @@ public class BaseDepthController { * @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float) */ private float mDepth; + // Used just for animating wallpaper zoom without affecting blur. + private final AnimatedFloat mWallpaperZoomOnly = new AnimatedFloat(this::applyDepthAndBlur); protected SurfaceControl mBaseSurface; @@ -125,7 +135,7 @@ public class BaseDepthController { */ // private EarlyWakeupInfo mEarlyWakeupInfo = new EarlyWakeupInfo(); - public BaseDepthController(QuickstepLauncher activity) { + public BaseDepthControllerImpl(QuickstepLauncher activity) { mLauncher = activity; if (Flags.allAppsBlur() || enableOverviewBackgroundWallpaperBlur()) { if (Utilities.ATLEAST_S) { @@ -141,12 +151,38 @@ public class BaseDepthController { } mWallpaperManager = activity.getSystemService(WallpaperManager.class); - MultiPropertyFactory depthProperty = + MultiPropertyFactory depthProperty = new MultiPropertyFactory<>(this, DEPTH, DEPTH_INDEX_COUNT, Float::max); stateDepth = depthProperty.get(DEPTH_INDEX_STATE_TRANSITION); widgetDepth = depthProperty.get(DEPTH_INDEX_WIDGET); + + if (Flags.enableExpressiveFolderExpansion()) { + MultiPropertyFactory folderZoomFactory = + new MultiPropertyFactory<>(mWallpaperZoomOnly, AnimatedFloat.VALUE, + ZOOM_INDEX_COUNT, Float::max); + folderZoom = folderZoomFactory.get(ZOOM_INDEX_FOLDER); + } else { + folderZoom = null; + } + // mEarlyWakeupInfo.token = new Binder(); -// mEarlyWakeupInfo.trace = BaseDepthController.class.getName(); +// mEarlyWakeupInfo.trace = BaseDepthControllerImpl.class.getName(); + } + + @Override + public MultiProperty getStateDepth() { + return stateDepth; + } + + @Override + public MultiProperty getWidgetDepth() { + return widgetDepth; + } + + @Override + public MultiProperty getFolderZoom() { + return folderZoom; + } } /** @@ -200,16 +236,20 @@ public class BaseDepthController { private void applyDepthAndBlurInternal(SurfaceControl.Transaction transaction, boolean applyImmediately, boolean skipSimilarBlur) { float depth = mDepth; + float wallpaperZoom = depth; + if (Flags.enableExpressiveFolderExpansion()) { + wallpaperZoom = Math.max(wallpaperZoom, mWallpaperZoomOnly.value); + } IBinder windowToken = mLauncher.getRootView().getWindowToken(); if (windowToken != null && Utilities.ATLEAST_R) { if (enableScalingRevealHomeAnimation()) { - mWallpaperManager.setWallpaperZoomOut(windowToken, depth); + mWallpaperManager.setWallpaperZoomOut(windowToken, wallpaperZoom); } else { // The API's full zoom-out is three times larger than the zoom-out we apply // to the // icons. To keep the two consistent throughout the animation while keeping // Launcher's concept of full depth unchanged, we divide the depth by 3 here. - mWallpaperManager.setWallpaperZoomOut(windowToken, depth / 3); + mWallpaperManager.setWallpaperZoomOut(windowToken, wallpaperZoom / 3); } } @@ -238,6 +278,7 @@ public class BaseDepthController { mCurrentBlur = newBlur; blurWorkspaceDepthTargets(); } + } if (!BlurUtils.supportsBlursOnWindows()) { return; @@ -253,7 +294,6 @@ public class BaseDepthController { return; } mWaitingOnSurfaceValidity = false; - SurfaceControl blurSurface = enableOverviewBackgroundWallpaperBlur() && mBlurSurface != null ? mBlurSurface : mBaseSurface; @@ -477,7 +517,6 @@ public class BaseDepthController { private static float mapDepthToBlur(float depth) { return Interpolators.clampToProgress(depth, 0, 0.3f); } - private SurfaceControl.Transaction createTransaction() { return new SurfaceControl.Transaction(); } diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt index a91ee3a0f4..d488f373e0 100644 --- a/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt +++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DepthControllerTest.kt @@ -16,35 +16,47 @@ package com.android.launcher3.statehandlers +import android.app.WallpaperManager import android.content.res.Resources +import android.os.IBinder +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.ViewTreeObserver import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.launcher3.Flags +import com.android.launcher3.Flags.FLAG_ENABLE_EXPRESSIVE_FOLDER_EXPANSION import com.android.launcher3.Launcher +import com.android.launcher3.LauncherRootView import com.android.launcher3.LauncherState import com.android.launcher3.R import com.android.launcher3.dragndrop.DragLayer import com.android.launcher3.statemanager.StateManager import com.android.launcher3.uioverrides.QuickstepLauncher -import java.util.Collections +import com.android.launcher3.views.ScrimView import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNull import junit.framework.TestCase.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.reset import org.mockito.Mockito.same import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import java.util.Collections @SmallTest @RunWith(AndroidJUnit4::class) class DepthControllerTest { + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule() private lateinit var underTest: DepthController @Mock private lateinit var launcher: QuickstepLauncher @@ -52,6 +64,10 @@ class DepthControllerTest { @Mock private lateinit var resource: Resources @Mock private lateinit var dragLayer: DragLayer @Mock private lateinit var viewTreeObserver: ViewTreeObserver + @Mock private lateinit var wallpaperManager: WallpaperManager + @Mock private lateinit var rootView: LauncherRootView + @Mock private lateinit var windowToken: IBinder + @Mock private lateinit var scrimView: ScrimView @Before fun setUp() { @@ -62,6 +78,13 @@ class DepthControllerTest { `when`(dragLayer.viewTreeObserver).thenReturn(viewTreeObserver) `when`(launcher.stateManager).thenReturn(stateManager) `when`(launcher.depthBlurTargets).thenReturn(Collections.emptyList()) + `when`(launcher.getSystemService(WallpaperManager::class.java)).thenReturn(wallpaperManager) + `when`(wallpaperManager.setWallpaperZoomOut(any(), anyFloat())).then { + // do nothing + } + `when`(launcher.rootView).thenReturn(rootView) + `when`(rootView.windowToken).thenReturn(windowToken) + `when`(launcher.scrimView).thenReturn(scrimView) underTest = DepthController(launcher) } @@ -205,4 +228,43 @@ class DepthControllerTest { `when`(stateManager.targetState).thenReturn(LauncherState.OVERVIEW) assertFalse(underTest.blurWorkspaceDepthTargets()) } + + @Test + @DisableFlags(FLAG_ENABLE_EXPRESSIVE_FOLDER_EXPANSION) + fun whenExpressiveFolderExpansionFalse_onlySetDepth() { + `when`(stateManager.state).thenReturn(LauncherState.NORMAL) + assertNull(underTest.folderZoom) + underTest.stateDepth.value = 1f + verify(wallpaperManager).setWallpaperZoomOut(windowToken, 1f) + } + + @Test + @EnableFlags(FLAG_ENABLE_EXPRESSIVE_FOLDER_EXPANSION) + fun wallpaperZoom_setsWallpaperZoom() { + val zoom = 0.75f + underTest.folderZoom.value = zoom + verify(wallpaperManager).setWallpaperZoomOut(windowToken, zoom) + } + + @Test + @EnableFlags(FLAG_ENABLE_EXPRESSIVE_FOLDER_EXPANSION) + fun wallpaperZoom_andStateDepth_setsMaxWallpaperZoom() { + `when`(stateManager.state).thenReturn(LauncherState.NORMAL) + val folderZoom = 0.8f + val stateDepth = 0.5f + underTest.stateDepth.value = stateDepth + underTest.folderZoom.value = folderZoom + + // wallpaperZoom should be max of folderZoom and stateDepth + verify(wallpaperManager, atLeastOnce()).setWallpaperZoomOut(windowToken, folderZoom) + + reset(wallpaperManager) + + val folderZoom2 = 0.6f + val stateDepth2 = 1f + underTest.folderZoom.value = folderZoom2 + underTest.stateDepth.value = stateDepth2 + + verify(wallpaperManager, atLeastOnce()).setWallpaperZoomOut(windowToken, stateDepth2) + } } diff --git a/src/com/android/launcher3/folder/FolderAnimationSpringBuilderManager.kt b/src/com/android/launcher3/folder/FolderAnimationSpringBuilderManager.kt index 4a32d8ba0b..c4dc195593 100644 --- a/src/com/android/launcher3/folder/FolderAnimationSpringBuilderManager.kt +++ b/src/com/android/launcher3/folder/FolderAnimationSpringBuilderManager.kt @@ -73,6 +73,8 @@ class FolderAnimationSpringBuilderManager( workspace.scaleY = 1f hotseat.scaleX = 1f hotseat.scaleY = 1f + + launcherDelegate.launcher?.depthController?.folderZoom?.value = 0f scrim.alpha = 1f scrim.setBackgroundColor(Color.TRANSPARENT) if (Utilities.ATLEAST_S && launcher.stateManager.state.getDepth(launcher) == 0f) { diff --git a/src/com/android/launcher3/folder/FolderSpringAnimatorSet.kt b/src/com/android/launcher3/folder/FolderSpringAnimatorSet.kt index 8998852a55..955cb619bb 100644 --- a/src/com/android/launcher3/folder/FolderSpringAnimatorSet.kt +++ b/src/com/android/launcher3/folder/FolderSpringAnimatorSet.kt @@ -37,6 +37,8 @@ import com.android.launcher3.Utilities.isDarkTheme import com.android.launcher3.anim.SpringAnimationBuilder import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW +import com.android.launcher3.util.MultiPropertyFactory +import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE /** Holder for Animators created from [FolderAnimationSpringBuilderManager] */ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) { @@ -55,6 +57,7 @@ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) { private const val DAMPING_ALPHA = 0.9f private const val STIFFNESS_LAUNCHER_SCRIM = 380f private const val DAMPING_LAUNCHER_SCRIM = 0.98f + private const val WALLPAPER_ZOOM = 0.125f /** * Factory method to take data calculated from [FolderAnimationSpringBuilderManager], and @@ -78,6 +81,14 @@ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) { folderAnimData.isOpening, launcherDelegate, ) + launcherDelegate.launcher?.depthController?.folderZoom?.let { + addWallpaperZoomAnimator( + folder.context, + animatorSet, + folderAnimData.isOpening, + it, + ) + } iconAnimData.forEach { addContentIconAnimators(folder.context, animatorSet, it) } return FolderSpringAnimatorSet(animatorSet) } @@ -224,6 +235,27 @@ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) { ) } + private fun addWallpaperZoomAnimator( + context: Context, + animatorSet: AnimatorSet, + isOpening: Boolean, + property: MultiPropertyFactory<*>.MultiProperty, + ) { + playSpringAnimation( + context = context, + animatorSet = animatorSet, + isOpening = isOpening, + startDelay = 0, + stiffness = STIFFNESS_SHAPE_POSITION, + damping = DAMPING_SHAPE_POSITION, + startValue = 0f, + endValue = WALLPAPER_ZOOM, + minVisibleChange = MIN_VISIBLE_CHANGE_SCALE, + property = MULTI_PROPERTY_VALUE, + view = property, + ) + } + private fun addAlphaAndColorAnimators( folder: Folder, animatorSet: AnimatorSet, diff --git a/src/com/android/launcher3/statehandlers/BaseDepthController.kt b/src/com/android/launcher3/statehandlers/BaseDepthController.kt new file mode 100644 index 0000000000..8e755f52a9 --- /dev/null +++ b/src/com/android/launcher3/statehandlers/BaseDepthController.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 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.launcher3.statehandlers + +import android.content.Context +import com.android.launcher3.anim.AnimatedFloat +import com.android.launcher3.statemanager.BaseState +import com.android.launcher3.statemanager.StatefulContainer +import com.android.launcher3.util.MultiPropertyFactory + +/** + * Base interface for controlling depth effects from Launcher3. + */ +interface BaseDepthController { + + /** + * Property for state transitions depth. + */ + val stateDepth: MultiPropertyFactory<*>.MultiProperty? + + /** + * Property for the widget picker depth. + */ + val widgetDepth: MultiPropertyFactory<*>.MultiProperty? + + /** + * Property for folder expansion wallpaper zoom. + */ + val folderZoom: MultiPropertyFactory<*>.MultiProperty? +} \ No newline at end of file diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index 26cd7829ef..0b867ca848 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -83,6 +83,7 @@ import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.statehandlers.BaseDepthController; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.util.ApplicationInfoWrapper; import com.android.launcher3.util.LauncherBindableItemsContainer; @@ -215,6 +216,10 @@ public interface ActivityContext extends SavedStateRegistryOwner { return null; } + default BaseDepthController getDepthController() { + return null; + } + /** @return {@code true} if all apps background blur is enabled */ default boolean isAllAppsBackgroundBlurEnabled() { return false;