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

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

Merge "[flexiglass] Add nested scrolling to NSSL and notif scrim in Flexiglass" into main

parents 72737bc4 7a1955b4
Loading
Loading
Loading
Loading
+77 −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.foundation.gestures.Orientation
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import com.android.compose.nestedscroll.PriorityNestedScrollConnection

/**
 * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
 * following way:
 * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and
 *   then allows scrolling of the children (usually the content).
 * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and
 *   then resets the [scrimOffset] to [maxScrimOffset].
 */
fun NotificationScrimNestedScrollConnection(
    scrimOffset: () -> Float,
    onScrimOffsetChanged: (Float) -> Unit,
    minScrimOffset: () -> Float,
    maxScrimOffset: Float,
    contentHeight: () -> Float,
    minVisibleScrimHeight: () -> Float,
): PriorityNestedScrollConnection {
    return PriorityNestedScrollConnection(
        orientation = Orientation.Vertical,
        // scrolling up and inner content is taller than the scrim, so scrim needs to
        // expand; content can scroll once scrim is at the minScrimOffset.
        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
            offsetAvailable < 0 &&
                offsetBeforeStart == 0f &&
                contentHeight() > minVisibleScrimHeight() &&
                scrimOffset() > minScrimOffset()
        },
        // scrolling down and content is done scrolling to top. After that, the scrim
        // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
        canStartPostScroll = { offsetAvailable, _ ->
            offsetAvailable > 0 && scrimOffset() < maxScrimOffset
        },
        canStartPostFling = { false },
        canContinueScroll = {
            val currentHeight = scrimOffset()
            minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
        },
        canScrollOnFling = true,
        onStart = { /* do nothing */},
        onScroll = { offsetAvailable ->
            val currentHeight = scrimOffset()
            val amountConsumed =
                if (offsetAvailable > 0) {
                    val amountLeft = maxScrimOffset - currentHeight
                    offsetAvailable.coerceAtMost(amountLeft)
                } else {
                    val amountLeft = minScrimOffset() - currentHeight
                    offsetAvailable.coerceAtLeast(amountLeft)
                }
            onScrimOffsetChanged(currentHeight + amountConsumed)
            amountConsumed
        },
        // Don't consume the velocity on pre/post fling
        onStop = { 0f },
    )
}
+118 −21
Original line number Diff line number Diff line
@@ -20,32 +20,53 @@ package com.android.systemui.notifications.ui.composable
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.height
import com.android.systemui.notifications.ui.composable.Notifications.Form
import com.android.systemui.scene.ui.composable.Gone
import com.android.systemui.scene.ui.composable.Shade
import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt

@@ -100,24 +121,77 @@ fun SceneScope.NotificationStack(
@Composable
fun SceneScope.NotificationScrollingStack(
    viewModel: NotificationsPlaceholderViewModel,
    maxScrimTop: () -> Float,
    modifier: Modifier = Modifier,
) {
    val density = LocalDensity.current
    val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
    val expansionFraction by viewModel.expandFraction.collectAsState(0f)

    val contentHeight by viewModel.intrinsicContentHeight.collectAsState()
    val navBarHeight =
        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
    val statusBarHeight =
        with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
    val displayCutoutHeight =
        with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
    val screenHeight =
        with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
            navBarHeight +
            maxOf(statusBarHeight, displayCutoutHeight)

    val expansionFraction by viewModel.expandFraction.collectAsState(0f)
    val contentHeight = viewModel.intrinsicContentHeight.collectAsState()

    // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
    // calculated in minScrimOffset. The scrim is the same height as the screen minus the
    // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
    // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
    // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
    // entire height of the scrim is visible on screen.
    val scrimOffset = remember { mutableStateOf(0f) }

    val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }

    // The minimum offset for the scrim. The scrim is considered fully expanded when it
    // is at this offset.
    val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }

    // The height of the scrim visible on screen when it is in its resting (collapsed) state.
    val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }

    // we are not scrolled to the top unless the scrim is at its maximum offset.
    LaunchedEffect(viewModel, scrimOffset) {
        snapshotFlow { scrimOffset.value >= 0f }
            .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
    }

    // if contentHeight drops below minimum visible scrim height while scrim is
    // expanded, reset scrim offset.
    LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
        snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
            .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
    }

    Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
        Spacer(
            modifier =
                Modifier.fillMaxSize()
                    .graphicsLayer {
                        shape = RoundedCornerShape(cornerRadius.dp)
                        clip = true
                    }
                    .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
        )
        Box(
            modifier =
            modifier
                .verticalNestedScrollToScene()
                .fillMaxWidth()
                .element(Notifications.Elements.NotificationScrim)
                Modifier.fillMaxSize()
                    .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
                    .graphicsLayer {
                        shape = RoundedCornerShape(cornerRadius.dp)
                        clip = true
                    alpha = expansionFraction
                        alpha =
                            if (layoutState.isTransitioningBetween(Gone, Shade)) {
                                (expansionFraction / 0.3f).coerceAtMost(1f)
                            } else 1f
                    }
                    .background(MaterialTheme.colorScheme.surface)
                    .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
@@ -125,8 +199,31 @@ fun SceneScope.NotificationScrollingStack(
            NotificationPlaceholder(
                viewModel = viewModel,
                form = Form.Stack,
            modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() }
                modifier =
                    Modifier.verticalNestedScrollToScene(
                            topBehavior = NestedScrollBehavior.EdgeWithPreview,
                        )
                        .nestedScroll(
                            remember(
                                scrimOffset,
                                maxScrimTop,
                                minScrimTop,
                            ) {
                                NotificationScrimNestedScrollConnection(
                                    scrimOffset = { scrimOffset.value },
                                    onScrimOffsetChanged = { scrimOffset.value = it },
                                    minScrimOffset = minScrimOffset,
                                    maxScrimOffset = 0f,
                                    contentHeight = { contentHeight.value },
                                    minVisibleScrimHeight = minVisibleScrimHeight,
                                )
                            }
                        )
                        .verticalScroll(rememberScrollState())
                        .fillMaxWidth()
                        .height { (contentHeight.value + navBarHeight).roundToInt() },
            )
        }
    }
}

+35 −5
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -46,6 +47,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -56,7 +62,7 @@ import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
@@ -116,6 +122,8 @@ private fun SceneScope.QuickSettingsScene(
    statusBarIconController: StatusBarIconController,
    modifier: Modifier = Modifier,
) {
    val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()

    // TODO(b/280887232): implement the real UI.
    Box(modifier = modifier.fillMaxSize()) {
        val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -234,10 +242,32 @@ private fun SceneScope.QuickSettingsScene(
                }
            }
        }
        HeadsUpNotificationSpace(
            viewModel = viewModel.notifications,
            isPeekFromBottom = true,
            modifier = Modifier.padding(16.dp).fillMaxSize(),
        // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
        // transition from Shade scene.
        Box(
            modifier =
                Modifier.element(Notifications.Elements.NotificationScrim)
                    .fillMaxWidth()
                    .height(0.dp)
                    .graphicsLayer {
                        shape = RoundedCornerShape(cornerRadius.dp)
                        clip = true
                        alpha = 1f
                    }
                    .background(MaterialTheme.colorScheme.surface)
                    .align(Alignment.BottomCenter)
                    .onPlaced { coordinates: LayoutCoordinates ->
                        viewModel.notifications.onContentTopChanged(
                            coordinates.positionInWindow().y
                        )
                        val boundsInWindow = coordinates.boundsInWindow()
                        viewModel.notifications.onBoundsChanged(
                            left = boundsInWindow.left,
                            top = boundsInWindow.top,
                            right = boundsInWindow.right,
                            bottom = boundsInWindow.bottom,
                        )
                    }
        )
    }
}
+1 −10
Original line number Diff line number Diff line
@@ -18,13 +18,10 @@ package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
@@ -66,12 +63,6 @@ constructor(
    override fun SceneScope.Content(
        modifier: Modifier,
    ) {
        Box(modifier = modifier) {
        Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
            HeadsUpNotificationSpace(
                viewModel = notificationsViewModel,
                modifier = Modifier.padding(16.dp).fillMaxSize(),
            )
        }
    }
}
+88 −44
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
@@ -62,6 +63,7 @@ import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.util.animation.MeasurementInput
import javax.inject.Inject
import javax.inject.Named
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -154,24 +156,34 @@ private fun SceneScope.ShadeScene(
    mediaHost: MediaHost,
    modifier: Modifier = Modifier,
) {
    val localDensity = LocalDensity.current
    val density = LocalDensity.current
    val layoutWidth = remember { mutableStateOf(0) }
    val maxNotifScrimTop = remember { mutableStateOf(0f) }

    Box(
        modifier =
            modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
    )
    Box {
        Layout(
            contents =
                listOf(
                    {
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() })
                            modifier =
                                Modifier.fillMaxWidth()
                                    .clickable(onClick = { viewModel.onContentClicked() })
                        ) {
                            CollapsedShadeHeader(
                                viewModel = viewModel.shadeHeaderViewModel,
                                createTintedIconManager = createTintedIconManager,
                                createBatteryMeterViewController = createBatteryMeterViewController,
                                statusBarIconController = statusBarIconController,
                modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
                                modifier =
                                    Modifier.padding(
                                        horizontal = Shade.Dimensions.HorizontalPadding
                                    )
                            )
                            QuickSettings(
                                modifier = Modifier.height(130.dp),
@@ -179,14 +191,17 @@ private fun SceneScope.ShadeScene(
                            )

                            if (viewModel.isMediaVisible()) {
                val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded)
                                val mediaHeight =
                                    dimensionResource(R.dimen.qs_media_session_height_expanded)
                                MediaCarousel(
                                    modifier =
                        Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints
                            ->
                                        Modifier.height(mediaHeight).fillMaxWidth().layout {
                                            measurable,
                                            constraints ->
                                            val placeable = measurable.measure(constraints)

                            // Notify controller to size the carousel for the current space
                                            // Notify controller to size the carousel for the
                                            // current space
                                            mediaHost.measurementInput =
                                                MeasurementInput(placeable.width, placeable.height)
                                            mediaCarouselController.setSceneContainerSize(
@@ -200,16 +215,45 @@ private fun SceneScope.ShadeScene(
                                        },
                                    mediaHost = mediaHost,
                                    layoutWidth = layoutWidth.value,
                    layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(),
                                    layoutHeight = with(density) { mediaHeight.toPx() }.toInt(),
                                    carouselController = mediaCarouselController,
                                )
                            }

                            Spacer(modifier = Modifier.height(16.dp))
                        }
                    },
                    {
                        NotificationScrollingStack(
                            viewModel = viewModel.notifications,
                modifier = Modifier.fillMaxWidth().weight(1f),
                            maxScrimTop = { maxNotifScrimTop.value },
                        )
                    },
                )
        ) { measurables, constraints ->
            check(measurables.size == 2)
            check(measurables[0].size == 1)
            check(measurables[1].size == 1)

            val quickSettingsPlaceable = measurables[0][0].measure(constraints)

            val notificationsMeasurable = measurables[1][0]
            val notificationsScrimMaxHeight =
                constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
            val notificationsPlaceable =
                notificationsMeasurable.measure(
                    constraints.copy(
                        minHeight = notificationsScrimMaxHeight,
                        maxHeight = notificationsScrimMaxHeight
                    )
                )

            maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()

            layout(constraints.maxWidth, constraints.maxHeight) {
                quickSettingsPlaceable.placeRelative(x = 0, y = 0)
                notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
            }
        }
    }
}
Loading