Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0bbf1dde authored by Shawn Lee's avatar Shawn Lee Committed by Android (Google) Code Review
Browse files

Merge changes I14b38827,I709b0e1f into main

* changes:
  [flexiglass] Fix bouncer to lockscreen transition
  [flexiglass] Implement Shade -> Lockscreen scene transition
parents 796d3ffc 78360d34
Loading
Loading
Loading
Loading
+10 −10
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModul
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -35,12 +36,7 @@ import dagger.multibindings.IntoSet
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi

@Module(
    includes =
        [
            LockscreenSceneBlueprintModule::class,
        ],
)
@Module(includes = [LockscreenSceneBlueprintModule::class])
interface LockscreenSceneModule {

    @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
@@ -51,9 +47,7 @@ interface LockscreenSceneModule {
        @Provides
        @SysUISingleton
        @KeyguardRootView
        fun viewProvider(
            configurator: Provider<KeyguardViewConfigurator>,
        ): () -> View {
        fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View {
            return { configurator.get().getKeyguardRootView() }
        }

@@ -67,10 +61,16 @@ interface LockscreenSceneModule {
        @Provides
        fun providesLockscreenContent(
            viewModelFactory: LockscreenContentViewModel.Factory,
            notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
            blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
            clockInteractor: KeyguardClockInteractor,
        ): LockscreenContent {
            return LockscreenContent(viewModelFactory, blueprints, clockInteractor)
            return LockscreenContent(
                viewModelFactory,
                notificationScrimViewModelFactory,
                blueprints,
                clockInteractor,
            )
        }
    }
}
+14 −5
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.NotificationLockscreenScrim
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel

/**
 * Renders the content of the lockscreen.
@@ -39,6 +41,7 @@ import com.android.systemui.lifecycle.rememberViewModel
 */
class LockscreenContent(
    private val viewModelFactory: LockscreenContentViewModel.Factory,
    private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
    private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
    private val clockInteractor: KeyguardClockInteractor,
) {
@@ -47,10 +50,13 @@ class LockscreenContent(
    }

    @Composable
    fun SceneScope.Content(
        modifier: Modifier = Modifier,
    ) {
        val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() }
    fun SceneScope.Content(modifier: Modifier = Modifier) {
        val viewModel =
            rememberViewModel("LockscreenContent-viewModel") { viewModelFactory.create() }
        val notificationLockscreenScrimViewModel =
            rememberViewModel("LockscreenContent-scrimViewModel") {
                notificationScrimViewModelFactory.create()
            }
        val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
        if (!isContentVisible) {
            // If the content isn't supposed to be visible, show a large empty box as it's needed
@@ -71,6 +77,9 @@ class LockscreenContent(
        }

        val blueprint = blueprintByBlueprintId[blueprintId] ?: return
        with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) }
        with(blueprint) {
            Content(viewModel, modifier.sysuiResTag("keyguard_root_view"))
            NotificationLockscreenScrim(notificationLockscreenScrimViewModel)
        }
    }
}
+119 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.notifications.ui.composable

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import kotlinx.coroutines.launch

/**
 * A full-screen notifications scrim that is only visible after transitioning from Shade scene to
 * Lockscreen Scene and ending user input, at which point it fades out, visually completing the
 * transition.
 */
@Composable
fun SceneScope.NotificationLockscreenScrim(
    viewModel: NotificationLockscreenScrimViewModel,
    modifier: Modifier = Modifier,
) {
    val coroutineScope = rememberCoroutineScope()
    val shadeMode = viewModel.shadeMode.collectAsStateWithLifecycle()

    // Important: Make sure that shouldShowScrimFadeOut() is checked the first time the Lockscreen
    // scene is composed.
    val useFadeOutOnComposition =
        remember(shadeMode.value) {
            layoutState.currentTransition?.let { currentTransition ->
                shouldShowScrimFadeOut(currentTransition, shadeMode.value)
            } ?: false
        }

    val alphaAnimatable = remember { Animatable(1f) }

    LaunchedEffect(
        alphaAnimatable,
        layoutState.currentTransition,
        useFadeOutOnComposition,
        shadeMode,
    ) {
        val currentTransition = layoutState.currentTransition
        if (
            useFadeOutOnComposition &&
                currentTransition != null &&
                shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
                currentTransition.isUserInputOngoing
        ) {
            // keep scrim visible until user lifts their finger.
            viewModel.setAlphaForLockscreenFadeIn(0f)
            alphaAnimatable.snapTo(1f)
        } else if (
            useFadeOutOnComposition &&
                (currentTransition == null ||
                    (shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
                        !currentTransition.isUserInputOngoing))
        ) {
            // we no longer want to keep the scrim from fading out, so animate the scrim fade-out
            // and pipe the progress to the view model as well, so NSSL can fade-in the stack in
            // tandem.
            viewModel.setAlphaForLockscreenFadeIn(0f)
            coroutineScope.launch {
                snapshotFlow { alphaAnimatable.value }
                    .collect { viewModel.setAlphaForLockscreenFadeIn(1 - it) }
            }
            alphaAnimatable.animateTo(0f, tween())
        } else {
            // disable the scrim fade logic.
            viewModel.setAlphaForLockscreenFadeIn(1f)
            alphaAnimatable.snapTo(0f)
        }
    }

    Box(
        modifier
            .fillMaxSize()
            .element(Notifications.Elements.NotificationScrim)
            .graphicsLayer { alpha = alphaAnimatable.value }
            .background(MaterialTheme.colorScheme.surface)
    )
}

private fun shouldShowScrimFadeOut(
    currentTransition: TransitionState.Transition,
    shadeMode: ShadeMode,
): Boolean {
    return shadeMode == ShadeMode.Single &&
        currentTransition.isInitiatedByUserInput &&
        (currentTransition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) ||
            currentTransition.isTransitioning(from = Scenes.Bouncer, to = Scenes.Lockscreen))
}
+39 −14
Original line number Diff line number Diff line
@@ -86,6 +86,8 @@ import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
@@ -101,7 +103,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

object Notifications {
@@ -251,6 +253,7 @@ fun SceneScope.ConstrainedNotificationStack(
        NotificationPlaceholder(
            stackScrollView = stackScrollView,
            viewModel = viewModel,
            useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) },
            modifier = Modifier.fillMaxSize(),
        )
        HeadsUpNotificationSpace(
@@ -363,7 +366,6 @@ fun SceneScope.NotificationScrollingStack(
        snapshotFlow { syntheticScroll.value }
            .collect { delta ->
                scrollNotificationStack(
                    scope = coroutineScope,
                    delta = delta,
                    animate = false,
                    scrimOffset = scrimOffset,
@@ -383,7 +385,6 @@ fun SceneScope.NotificationScrollingStack(
                // composed at least once), and our remote input row overlaps with the ime bounds.
                if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
                    scrollNotificationStack(
                        scope = coroutineScope,
                        delta = remoteInputRowBottom - imeTopValue,
                        animate = true,
                        scrimOffset = scrimOffset,
@@ -450,7 +451,10 @@ fun SceneScope.NotificationScrollingStack(
                                scrimCornerRadius,
                                screenCornerRadius,
                                { expansionFraction },
                                shouldAnimateScrimCornerRadius(
                                    layoutState,
                                    shouldPunchHoleBehindScrim,
                                ),
                            )
                            .let { scrimRounding.value.toRoundedCornerShape(it) }
                    clip = true
@@ -514,6 +518,9 @@ fun SceneScope.NotificationScrollingStack(
                NotificationPlaceholder(
                    stackScrollView = stackScrollView,
                    viewModel = viewModel,
                    useStackBounds = {
                        !shouldUseLockscreenStackBounds(layoutState.transitionState)
                    },
                    modifier =
                        Modifier.notificationStackHeight(
                                view = stackScrollView,
@@ -600,6 +607,7 @@ fun SceneScope.NotificationStackCutoffGuideline(
private fun SceneScope.NotificationPlaceholder(
    stackScrollView: NotificationScrollView,
    viewModel: NotificationsPlaceholderViewModel,
    useStackBounds: () -> Boolean,
    modifier: Modifier = Modifier,
) {
    Box(
@@ -609,6 +617,12 @@ private fun SceneScope.NotificationPlaceholder(
                .debugBackground(viewModel, DEBUG_STACK_COLOR)
                .onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } }
                .onGloballyPositioned { coordinates: LayoutCoordinates ->
                    // This element is opted out of the shared element system, so there can be
                    // multiple instances of it during a transition. Thus we need to determine which
                    // instance should feed its bounds to NSSL to avoid providing conflicting values
                    val useBounds = useStackBounds()
                    if (useBounds) {
                        // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top won't
                        val positionInWindow = coordinates.positionInWindow()
                        debugLog(viewModel) {
                            "STACK onGloballyPositioned:" +
@@ -616,14 +630,13 @@ private fun SceneScope.NotificationPlaceholder(
                                " position=$positionInWindow" +
                                " bounds=${coordinates.boundsInWindow()}"
                        }
                    // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
                        stackScrollView.setStackTop(positionInWindow.y)
                    }
                }
    )
}

private suspend fun scrollNotificationStack(
    scope: CoroutineScope,
    delta: Float,
    animate: Boolean,
    scrimOffset: Animatable<Float, AnimationVector1D>,
@@ -638,7 +651,7 @@ private suspend fun scrollNotificationStack(
            if (animate) {
                // launch a new coroutine for the remainder animation so that it doesn't suspend the
                // scrim animation, allowing both to play simultaneously.
                scope.launch { scrollState.animateScrollTo(remainingDelta) }
                coroutineScope { launch { scrollState.animateScrollTo(remainingDelta) } }
            } else {
                scrollState.scrollTo(remainingDelta)
            }
@@ -658,6 +671,18 @@ private suspend fun scrollNotificationStack(
    }
}

private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean {
    return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen
}

private fun shouldAnimateScrimCornerRadius(
    state: SceneTransitionLayoutState,
    shouldPunchHoleBehindScrim: Boolean,
): Boolean {
    return shouldPunchHoleBehindScrim ||
        state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
}

private fun calculateCornerRadius(
    scrimCornerRadius: Dp,
    screenCornerRadius: Dp,
+5 −0
Original line number Diff line number Diff line
@@ -82,6 +82,10 @@ val SceneContainerTransitions = transitions {
        sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
    }
    from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
    from(Scenes.Shade, to = Scenes.Lockscreen) {
        reversed { lockscreenToShadeTransition() }
        sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false)
    }

    // Overlay transitions

@@ -94,6 +98,7 @@ val SceneContainerTransitions = transitions {
    // Scene overscroll

    overscrollDisabled(Scenes.Gone, Orientation.Vertical)
    overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical)
    overscroll(Scenes.Bouncer, Orientation.Vertical) {
        translate(Bouncer.Elements.Content, y = { absoluteDistance })
    }
Loading