Add functionality to DepthController to control wallpaper zoom indepedently of blur

Also add wallpaper zoom to folder motion

Bug: b/387543793
Flag: com.android.launcher3.enable_expressive_folder_expansion
Test: manually and DepthControllerTest
Change-Id: I94142806ffb6dfb93d363017ef4d13b3a5e74681
This commit is contained in:
Charlie Anderson authored and Pun Butrach committed 2026-10-26 00:08:46 +07:00
1 parent 0d40f78616
commit e9c655dbf6
8 files changed
+200 -16

No files matched your search

@@ -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<LauncherState>,
public class DepthController extends BaseDepthControllerImpl implements StateHandler<LauncherState>,
BaseActivity.MultiWindowModeChangedListener {
@VisibleForTesting
final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw;
@@ -1335,6 +1335,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
out.add(new RecentsViewStateController(this));
}
@Override
public DepthController getDepthController() {
return mDepthController;
}
@@ -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<BaseDepthController> DEPTH =
new FloatProperty<BaseDepthController>("depth") {
// Includes both Wallpaper Zoom and Blur
private static final FloatProperty<BaseDepthControllerImpl> DEPTH =
new FloatProperty<BaseDepthControllerImpl>("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<BaseDepthController> depthProperty =
MultiPropertyFactory<BaseDepthControllerImpl> 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<AnimatedFloat> 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();
}
@@ -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)
}
}
@@ -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) {
@@ -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,
@@ -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?
}
@@ -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;