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

Commit 545ff286 authored by Lucas Silva's avatar Lucas Silva
Browse files

Update responsive grid layout + alpha animation

This change updates the layout to be based on the screen size instead of
the available size. This is needed to ensure we always use the same
number of items in landscape/portrait, as the screen size is guaranteed
to not change between orientations. However, the available size can
change depending on the placement of items on the screen.

We also add an alpha animation to hide partial columns and fade them in
as the user scrolls.

Bug: 378171351
Test: manually on folded + unfolded foldable device in both orientations
Flag: com.android.systemui.communal_responsive_grid
Change-Id: I978a881371609dbf194dbfaec6ac23fc0e8b7357
parent 4f8a4c44
Loading
Loading
Loading
Loading
+56 −1
Original line number Diff line number Diff line
@@ -151,6 +151,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -947,12 +948,28 @@ private fun BoxScope.CommunalHubLazyGrid(
                    }
                }
            } else {
                val itemAlpha =
                    if (communalResponsiveGrid()) {
                        val percentVisible by
                            remember(gridState, index) {
                                derivedStateOf { calculatePercentVisible(gridState, index) }
                            }
                        animateFloatAsState(percentVisible)
                    } else {
                        null
                    }

                CommunalContent(
                    model = item,
                    viewModel = viewModel,
                    size = size,
                    selected = false,
                    modifier = Modifier.requiredSize(dpSize).animateItem(),
                    modifier =
                        Modifier.requiredSize(dpSize).animateItem().thenIf(
                            communalResponsiveGrid()
                        ) {
                            Modifier.graphicsLayer { alpha = itemAlpha?.value ?: 1f }
                        },
                    index = index,
                    contentListState = contentListState,
                    interactionHandler = interactionHandler,
@@ -1856,6 +1873,44 @@ private fun CommunalContentModel.getSpanOrMax(maxSpan: Int?) =
        size.span
    }

private fun IntRect.percentOverlap(other: IntRect): Float {
    val intersection = intersect(other)
    if (intersection.width < 0 || intersection.height < 0) {
        return 0f
    }
    val overlapArea = intersection.width * intersection.height
    val area = width * height
    return overlapArea.toFloat() / area.toFloat()
}

private fun calculatePercentVisible(state: LazyGridState, index: Int): Float {
    val viewportSize = state.layoutInfo.viewportSize
    val visibleRect =
        IntRect(
            offset =
                IntOffset(
                    state.layoutInfo.viewportStartOffset + state.layoutInfo.beforeContentPadding,
                    0,
                ),
            size =
                IntSize(
                    width =
                        viewportSize.width -
                            state.layoutInfo.beforeContentPadding -
                            state.layoutInfo.afterContentPadding,
                    height = viewportSize.height,
                ),
        )

    val itemInfo = state.layoutInfo.visibleItemsInfo.find { it.index == index }
    return if (itemInfo != null) {
        val boundingBox = IntRect(itemInfo.offset, itemInfo.size)
        boundingBox.percentOverlap(visibleRect)
    } else {
        0f
    }
}

private object Colors {
    val DisabledColorFilter by lazy { disabledColorMatrix() }

+42 −10
Original line number Diff line number Diff line
@@ -36,7 +36,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
@@ -45,6 +47,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.window.layout.WindowMetricsCalculator

/**
 * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain
@@ -71,7 +74,7 @@ fun ResponsiveLazyHorizontalGrid(
            "$minHorizontalArrangement and $minVerticalArrangement, respectively."
    }
    BoxWithConstraints(modifier) {
        val gridSize = rememberGridSize(maxWidth = maxWidth, maxHeight = maxHeight)
        val gridSize = rememberGridSize()
        val layoutDirection = LocalLayoutDirection.current
        val density = LocalDensity.current

@@ -128,25 +131,43 @@ fun ResponsiveLazyHorizontalGrid(
        val extraWidth = maxWidth - usedWidth
        val extraHeight = maxHeight - usedHeight

        val finalStartPadding = minStartPadding + extraWidth / 2
        // If there is a single column or single row, distribute extra space evenly across the grid.
        // Otherwise, distribute it along the content padding to center the content.
        val distributeHorizontalSpaceAlongGutters = gridSize.height == 1 || gridSize.width == 1
        val evenlyDistributedWidth =
            if (distributeHorizontalSpaceAlongGutters) {
                extraWidth / (gridSize.width + 1)
            } else {
                extraWidth / 2
            }

        val finalStartPadding = minStartPadding + evenlyDistributedWidth
        val finalEndPadding = minEndPadding + evenlyDistributedWidth
        val finalTopPadding = minTopPadding + extraHeight / 2

        val finalContentPadding =
            PaddingValues(
                start = finalStartPadding,
                end = minEndPadding + extraWidth / 2,
                end = finalEndPadding,
                top = finalTopPadding,
                bottom = minBottomPadding + extraHeight / 2,
            )

        with(density) { setContentOffset(Offset(finalStartPadding.toPx(), finalTopPadding.toPx())) }

        val horizontalArrangement =
            if (distributeHorizontalSpaceAlongGutters) {
                minHorizontalArrangement + evenlyDistributedWidth
            } else {
                minHorizontalArrangement
            }

        LazyHorizontalGrid(
            rows = GridCells.Fixed(gridSize.height),
            modifier = Modifier.fillMaxSize(),
            state = state,
            contentPadding = finalContentPadding,
            horizontalArrangement = Arrangement.spacedBy(minHorizontalArrangement),
            horizontalArrangement = Arrangement.spacedBy(horizontalArrangement),
            verticalArrangement = Arrangement.spacedBy(minVerticalArrangement),
            flingBehavior = flingBehavior,
            userScrollEnabled = userScrollEnabled,
@@ -210,27 +231,38 @@ data class SizeInfo(
}

@Composable
private fun rememberGridSize(maxWidth: Dp, maxHeight: Dp): IntSize {
private fun rememberGridSize(): IntSize {
    val configuration = LocalConfiguration.current
    val orientation = configuration.orientation
    val screenSize = calculateWindowSize()

    return remember(orientation, maxWidth, maxHeight) {
    return remember(orientation, screenSize) {
        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
            IntSize(
                width = calculateNumCellsWidth(maxWidth),
                height = calculateNumCellsHeight(maxHeight),
                width = calculateNumCellsWidth(screenSize.width),
                height = calculateNumCellsHeight(screenSize.height),
            )
        } else {
            // In landscape we invert the rows/columns to ensure we match the same area as portrait.
            // This keeps the number of elements in the grid consistent when changing orientation.
            IntSize(
                width = calculateNumCellsHeight(maxWidth),
                height = calculateNumCellsWidth(maxHeight),
                width = calculateNumCellsHeight(screenSize.width),
                height = calculateNumCellsWidth(screenSize.height),
            )
        }
    }
}

@Composable
fun calculateWindowSize(): DpSize {
    // Observe view configuration changes and recalculate the size class on each change.
    LocalConfiguration.current
    val density = LocalDensity.current
    val context = LocalContext.current
    val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
    return with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
}

private fun calculateNumCellsWidth(width: Dp) =
    // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
    when {