Loading packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRe import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import java.util.Optional import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import org.junit.Before Loading Loading @@ -71,6 +72,7 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { viewModelFactory = viewModelFactory, squeezeEffectInteractor = SqueezeEffectInteractor(squeezeEffectRepository = fakeSqueezeEffectRepository), appZoomOutOptional = Optional.empty(), ) } Loading packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt +5 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,8 @@ import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope Loading @@ -50,6 +52,8 @@ constructor( private val squeezeEffectInteractor: SqueezeEffectInteractor, private val keyEventInteractor: KeyEventInteractor, private val viewModelFactory: SqueezeEffectViewModel.Factory, //TODO(b/409930584): make AppZoomOut non-optional private val appZoomOutOptional: Optional<AppZoomOut>, ) : CoreStartable { private var root: EffectsWindowRoot? = null Loading Loading @@ -108,6 +112,7 @@ constructor( root = null } }, appZoomOutOptional = appZoomOutOptional, ) root?.let { rootView -> windowManager.addView(rootView, getWindowManagerLayoutParams()) Loading packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt +4 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.AbstractComposeView import com.android.systemui.compose.ComposeInitializer import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional @SuppressLint("ViewConstructor") class EffectsWindowRoot( Loading @@ -32,6 +34,7 @@ class EffectsWindowRoot( @DrawableRes private val topRoundedCornerResourceId: Int, @DrawableRes private val bottomRoundedCornerResourceId: Int, private val physicalPixelDisplaySizeRatio: Float, private val appZoomOutOptional: Optional<AppZoomOut>, ) : AbstractComposeView(context) { override fun onAttachedToWindow() { Loading @@ -52,6 +55,7 @@ class EffectsWindowRoot( topRoundedCornerResourceId = topRoundedCornerResourceId, bottomRoundedCornerResourceId = bottomRoundedCornerResourceId, physicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio, appZoomOutOptional = appZoomOutOptional, ) } } packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt +50 −18 Original line number Diff line number Diff line Loading @@ -37,12 +37,19 @@ import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel private val SqueezeEffectMaxThickness = 12.dp import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel.Companion.ZOOM_OUT_SCALE import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional // Defines the amount the squeeze border overlaps the shrinking content. // This is the difference between the total squeeze thickness and the thickness purely caused by the // zoom effect. At full progress, this overlap is 8 dp. private val SqueezeEffectOverlapMaxThickness = 8.dp private val SqueezeColor = Color.Black @Composable Loading @@ -52,6 +59,7 @@ fun SqueezeEffect( @DrawableRes bottomRoundedCornerResourceId: Int, physicalPixelDisplaySizeRatio: Float, onEffectFinished: () -> Unit, appZoomOutOptional: Optional<AppZoomOut>, ) { val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() } Loading Loading @@ -105,56 +113,80 @@ fun SqueezeEffect( } } LaunchedEffect(squeezeProgress.value) { appZoomOutOptional.ifPresent { it.setTopLevelScale(1f - squeezeProgress.value * ZOOM_OUT_SCALE) } } val screenWidth = LocalWindowInfo.current.containerSize.width val screenHeight = LocalWindowInfo.current.containerSize.height Canvas(modifier = Modifier.fillMaxSize()) { if (squeezeProgress.value <= 0) { return@Canvas } val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress.value // Calculate the thickness of the squeeze effect borders. // The total thickness on each side is composed of two parts: // 1. Zoom Thickness: This accounts for the visual space created by the AppZoomOut // effect scaling the content down. It's calculated as half the total reduction // in screen dimension (width or height) caused by scaling (ZOOM_OUT_SCALE), // proportional to the current squeezeProgress. We divide by 2 because the // reduction happens on both sides (left/right or top/bottom). // 2. Overlap Thickness: An additional fixed thickness (converted from dp to px) // scaled by the squeezeProgress, designed to make the border slightly overlap // the scaled content for a better visual effect. val horizontalZoomThickness = screenWidth * ZOOM_OUT_SCALE * squeezeProgress.value / 2f val verticalZoomThickness = screenHeight * ZOOM_OUT_SCALE * squeezeProgress.value / 2f val overlapThickness = SqueezeEffectOverlapMaxThickness.toPx() * squeezeProgress.value val horizontalSqueezeThickness = horizontalZoomThickness + overlapThickness val verticalSqueezeThickness = verticalZoomThickness + overlapThickness drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness)) drawRect(color = SqueezeColor, size = Size(size.width, verticalSqueezeThickness)) drawRect( color = SqueezeColor, topLeft = Offset(0f, size.height - squeezeThickness), size = Size(size.width, squeezeThickness), topLeft = Offset(0f, size.height - verticalSqueezeThickness), size = Size(size.width, verticalSqueezeThickness), ) drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height)) drawRect(color = SqueezeColor, size = Size(horizontalSqueezeThickness, size.height)) drawRect( color = SqueezeColor, topLeft = Offset(size.width - squeezeThickness, 0f), size = Size(squeezeThickness, size.height), topLeft = Offset(size.width - horizontalSqueezeThickness, 0f), size = Size(horizontalSqueezeThickness, size.height), ) drawTransform( dx = squeezeThickness, dy = squeezeThickness, dx = horizontalSqueezeThickness, dy = verticalSqueezeThickness, rotation = 0f, corner = top, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = size.width - squeezeThickness, dy = squeezeThickness, dx = size.width - horizontalSqueezeThickness, dy = verticalSqueezeThickness, rotation = 90f, corner = top, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = squeezeThickness, dy = size.height - squeezeThickness, dx = horizontalSqueezeThickness, dy = size.height - verticalSqueezeThickness, rotation = 270f, corner = bottom, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = size.width - squeezeThickness, dy = size.height - squeezeThickness, dx = size.width - horizontalSqueezeThickness, dy = size.height - verticalSqueezeThickness, rotation = 180f, corner = bottom, displaySizeRatio = physicalPixelDisplaySizeRatio, Loading packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt +17 −14 Original line number Diff line number Diff line Loading @@ -23,21 +23,20 @@ import com.android.systemui.lifecycle.Hydrator import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject class SqueezeEffectViewModel @AssistedInject constructor( keyEventInteractor: KeyEventInteractor ) : ExclusiveActivatable() { class SqueezeEffectViewModel @AssistedInject constructor(keyEventInteractor: KeyEventInteractor) : ExclusiveActivatable() { private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator") val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( traceName = "isPowerButtonPressed", source = keyEventInteractor.isPowerButtonDown source = keyEventInteractor.isPowerButtonDown, ) val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( traceName = "isPowerButtonLongPressed", source = keyEventInteractor.isPowerButtonLongPressed source = keyEventInteractor.isPowerButtonLongPressed, ) override suspend fun onActivated(): Nothing { Loading @@ -48,4 +47,8 @@ constructor( interface Factory { fun create(): SqueezeEffectViewModel } companion object { const val ZOOM_OUT_SCALE = 0.05f } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRe import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import java.util.Optional import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import org.junit.Before Loading Loading @@ -71,6 +72,7 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { viewModelFactory = viewModelFactory, squeezeEffectInteractor = SqueezeEffectInteractor(squeezeEffectRepository = fakeSqueezeEffectRepository), appZoomOutOptional = Optional.empty(), ) } Loading
packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt +5 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,8 @@ import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope Loading @@ -50,6 +52,8 @@ constructor( private val squeezeEffectInteractor: SqueezeEffectInteractor, private val keyEventInteractor: KeyEventInteractor, private val viewModelFactory: SqueezeEffectViewModel.Factory, //TODO(b/409930584): make AppZoomOut non-optional private val appZoomOutOptional: Optional<AppZoomOut>, ) : CoreStartable { private var root: EffectsWindowRoot? = null Loading Loading @@ -108,6 +112,7 @@ constructor( root = null } }, appZoomOutOptional = appZoomOutOptional, ) root?.let { rootView -> windowManager.addView(rootView, getWindowManagerLayoutParams()) Loading
packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt +4 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.AbstractComposeView import com.android.systemui.compose.ComposeInitializer import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional @SuppressLint("ViewConstructor") class EffectsWindowRoot( Loading @@ -32,6 +34,7 @@ class EffectsWindowRoot( @DrawableRes private val topRoundedCornerResourceId: Int, @DrawableRes private val bottomRoundedCornerResourceId: Int, private val physicalPixelDisplaySizeRatio: Float, private val appZoomOutOptional: Optional<AppZoomOut>, ) : AbstractComposeView(context) { override fun onAttachedToWindow() { Loading @@ -52,6 +55,7 @@ class EffectsWindowRoot( topRoundedCornerResourceId = topRoundedCornerResourceId, bottomRoundedCornerResourceId = bottomRoundedCornerResourceId, physicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio, appZoomOutOptional = appZoomOutOptional, ) } }
packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt +50 −18 Original line number Diff line number Diff line Loading @@ -37,12 +37,19 @@ import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel private val SqueezeEffectMaxThickness = 12.dp import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel.Companion.ZOOM_OUT_SCALE import com.android.wm.shell.appzoomout.AppZoomOut import java.util.Optional // Defines the amount the squeeze border overlaps the shrinking content. // This is the difference between the total squeeze thickness and the thickness purely caused by the // zoom effect. At full progress, this overlap is 8 dp. private val SqueezeEffectOverlapMaxThickness = 8.dp private val SqueezeColor = Color.Black @Composable Loading @@ -52,6 +59,7 @@ fun SqueezeEffect( @DrawableRes bottomRoundedCornerResourceId: Int, physicalPixelDisplaySizeRatio: Float, onEffectFinished: () -> Unit, appZoomOutOptional: Optional<AppZoomOut>, ) { val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() } Loading Loading @@ -105,56 +113,80 @@ fun SqueezeEffect( } } LaunchedEffect(squeezeProgress.value) { appZoomOutOptional.ifPresent { it.setTopLevelScale(1f - squeezeProgress.value * ZOOM_OUT_SCALE) } } val screenWidth = LocalWindowInfo.current.containerSize.width val screenHeight = LocalWindowInfo.current.containerSize.height Canvas(modifier = Modifier.fillMaxSize()) { if (squeezeProgress.value <= 0) { return@Canvas } val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress.value // Calculate the thickness of the squeeze effect borders. // The total thickness on each side is composed of two parts: // 1. Zoom Thickness: This accounts for the visual space created by the AppZoomOut // effect scaling the content down. It's calculated as half the total reduction // in screen dimension (width or height) caused by scaling (ZOOM_OUT_SCALE), // proportional to the current squeezeProgress. We divide by 2 because the // reduction happens on both sides (left/right or top/bottom). // 2. Overlap Thickness: An additional fixed thickness (converted from dp to px) // scaled by the squeezeProgress, designed to make the border slightly overlap // the scaled content for a better visual effect. val horizontalZoomThickness = screenWidth * ZOOM_OUT_SCALE * squeezeProgress.value / 2f val verticalZoomThickness = screenHeight * ZOOM_OUT_SCALE * squeezeProgress.value / 2f val overlapThickness = SqueezeEffectOverlapMaxThickness.toPx() * squeezeProgress.value val horizontalSqueezeThickness = horizontalZoomThickness + overlapThickness val verticalSqueezeThickness = verticalZoomThickness + overlapThickness drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness)) drawRect(color = SqueezeColor, size = Size(size.width, verticalSqueezeThickness)) drawRect( color = SqueezeColor, topLeft = Offset(0f, size.height - squeezeThickness), size = Size(size.width, squeezeThickness), topLeft = Offset(0f, size.height - verticalSqueezeThickness), size = Size(size.width, verticalSqueezeThickness), ) drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height)) drawRect(color = SqueezeColor, size = Size(horizontalSqueezeThickness, size.height)) drawRect( color = SqueezeColor, topLeft = Offset(size.width - squeezeThickness, 0f), size = Size(squeezeThickness, size.height), topLeft = Offset(size.width - horizontalSqueezeThickness, 0f), size = Size(horizontalSqueezeThickness, size.height), ) drawTransform( dx = squeezeThickness, dy = squeezeThickness, dx = horizontalSqueezeThickness, dy = verticalSqueezeThickness, rotation = 0f, corner = top, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = size.width - squeezeThickness, dy = squeezeThickness, dx = size.width - horizontalSqueezeThickness, dy = verticalSqueezeThickness, rotation = 90f, corner = top, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = squeezeThickness, dy = size.height - squeezeThickness, dx = horizontalSqueezeThickness, dy = size.height - verticalSqueezeThickness, rotation = 270f, corner = bottom, displaySizeRatio = physicalPixelDisplaySizeRatio, ) drawTransform( dx = size.width - squeezeThickness, dy = size.height - squeezeThickness, dx = size.width - horizontalSqueezeThickness, dy = size.height - verticalSqueezeThickness, rotation = 180f, corner = bottom, displaySizeRatio = physicalPixelDisplaySizeRatio, Loading
packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt +17 −14 Original line number Diff line number Diff line Loading @@ -23,21 +23,20 @@ import com.android.systemui.lifecycle.Hydrator import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject class SqueezeEffectViewModel @AssistedInject constructor( keyEventInteractor: KeyEventInteractor ) : ExclusiveActivatable() { class SqueezeEffectViewModel @AssistedInject constructor(keyEventInteractor: KeyEventInteractor) : ExclusiveActivatable() { private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator") val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( traceName = "isPowerButtonPressed", source = keyEventInteractor.isPowerButtonDown source = keyEventInteractor.isPowerButtonDown, ) val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( traceName = "isPowerButtonLongPressed", source = keyEventInteractor.isPowerButtonLongPressed source = keyEventInteractor.isPowerButtonLongPressed, ) override suspend fun onActivated(): Nothing { Loading @@ -48,4 +47,8 @@ constructor( interface Factory { fun create(): SqueezeEffectViewModel } companion object { const val ZOOM_OUT_SCALE = 0.05f } }