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

Commit 38ea04a0 authored by Olivier St-Onge's avatar Olivier St-Onge Committed by Android (Google) Code Review
Browse files

Merge changes from topic "406007592" into main

* changes:
  Implement new pager dots UI
  Update the number of quick settings columns for large screens in landscape
parents 8a1f5019 e986d5f1
Loading
Loading
Loading
Loading
+77 −88
Original line number Diff line number Diff line
@@ -16,25 +16,20 @@

package com.android.systemui.common.ui.compose

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.semantics.pageLeft
import androidx.compose.ui.semantics.pageRight
@@ -43,11 +38,11 @@ import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.android.app.tracing.coroutines.launchTraced as launch
import kotlin.math.absoluteValue
import com.android.compose.modifiers.width
import com.android.systemui.common.ui.compose.PagerDotsDefaults.SPRING_STIFFNESS
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import platform.test.motion.compose.values.MotionTestValueKey
import platform.test.motion.compose.values.motionTestValues
import kotlinx.coroutines.launch

@Composable
fun PagerDots(
@@ -58,88 +53,83 @@ fun PagerDots(
    dotSize: Dp = 6.dp,
    spaceSize: Dp = 4.dp,
) {
    if (pagerState.pageCount < 2) {
        return
    }
    val inPageTransition by
        remember(pagerState) {
            derivedStateOf {
                pagerState.currentPageOffsetFraction.absoluteValue > 0.05 &&
                    !pagerState.isOverscrolling()
            }
        }
    if (pagerState.pageCount < 2) return

    val activeDotWidth = dotSize * 2
    // Active dot + inactive dots + spacing
    val totalWidth =
        activeDotWidth +
            (dotSize * (pagerState.pageCount - 1)) +
            (spaceSize * (pagerState.pageCount - 1))
    val coroutineScope = rememberCoroutineScope()
    val doubleDotWidth = dotSize * 2 + spaceSize
    val activeMarkerWidth by
        animateDpAsState(
            targetValue = if (inPageTransition) doubleDotWidth else dotSize,
            label = "PagerDotsTransitionAnimation",
        )
    val cornerRadius = dotSize / 2

    fun DrawScope.drawDoubleRect(withPrevious: Boolean, width: Dp) {
        drawRoundRect(
            topLeft =
                Offset(
                    if (withPrevious) {
                        dotSize.toPx() - width.toPx()
    // List of animated colors, one per page
    val colors =
        remember(pagerState.pageCount) {
            List(pagerState.pageCount) { page ->
                Animatable(if (page == pagerState.currentPage) activeColor else nonActiveColor)
            }
        }
    LaunchedEffect(pagerState.currentPage, colors) {
        colors.forEachIndexed { index, animatable ->
            val targetColor =
                if (index == pagerState.currentPage) {
                    activeColor
                } else {
                        -(dotSize.toPx() + spaceSize.toPx())
                    },
                    0f,
                ),
            color = activeColor,
            size = Size(width.toPx(), dotSize.toPx()),
            cornerRadius = CornerRadius(cornerRadius.toPx()),
                    nonActiveColor
                }
            if (animatable.targetValue != targetColor) {
                launch {
                    animatable.animateTo(
                        targetColor,
                        animationSpec = spring(stiffness = SPRING_STIFFNESS),
                    )
                }
            }
        }
    }

    Row(
        modifier =
    Canvas(
        modifier
                .motionTestValues { activeMarkerWidth exportAs PagerDotsMotionKeys.indicatorWidth }
                .wrapContentWidth()
                .pagerDotsSemantics(pagerState, coroutineScope),
        horizontalArrangement = spacedBy(spaceSize),
        verticalAlignment = Alignment.CenterVertically,
            .width { totalWidth.roundToPx() }
            .height(dotSize)
            .pagerDotsSemantics(pagerState, coroutineScope)
    ) {
        // This means that the active rounded rect has to be drawn between the current page
        // and the previous one (as we are animating back), or the current one if not transitioning
        val withPrevious by
            remember(pagerState) {
                derivedStateOf {
                    pagerState.currentPageOffsetFraction <= 0 || pagerState.isOverscrolling()
                }
            }
        repeat(pagerState.pageCount) { page ->
            Canvas(Modifier.size(dotSize)) {
        val rtl = layoutDirection == LayoutDirection.Rtl
        scale(if (rtl) -1f else 1f, 1f, Offset(0f, center.y)) {
                    drawCircle(nonActiveColor)
                    // We always want to draw the rounded rect on the rightmost dot iteration, so
                    // the inactive dot is always drawn behind.
                    // This means that:
                    // * if we are scrolling back, we draw it when we are in the current page (so it
                    //   extends between this page and the previous one).
                    // * if we are scrolling forward, we draw it when we are in the next page (so it
                    //   extends between the next page and the current one).
                    // * if we are not scrolling, withPrevious is true (pageOffset 0) and we
                    //   draw in the current page.
                    // drawDoubleRect calculates the offset based on the above.
                    if (
                        withPrevious && page == pagerState.currentPage ||
                            (!withPrevious && page == pagerState.currentPage + 1)
                    ) {
                        drawDoubleRect(withPrevious, activeMarkerWidth)
                    }
            // The impacted index is the neighbor of the active index, in the direction dictated
            // from the page offset. The impacted dot will have its width modified
            val impactedIndex =
                if (pagerState.currentPageOffsetFraction >= 0) {
                    pagerState.currentPage + 1
                } else {
                    pagerState.currentPage - 1
                }
            val dotSizePx = dotSize.toPx()
            val activeDotWidthPx = activeDotWidth.toPx()
            val spacingPx = spaceSize.toPx()
            val offsetFraction = abs(pagerState.currentPageOffsetFraction)
            val cornerRadius = CornerRadius(size.height / 2)

            var x = 0f
            repeat(pagerState.pageCount) { page ->
                val width =
                    when (page) {
                        impactedIndex -> dotSizePx + dotSizePx * offsetFraction
                        pagerState.currentPage -> activeDotWidthPx - dotSizePx * offsetFraction
                        else -> dotSizePx
                    }

                drawRoundRect(
                    color = colors[page].value,
                    cornerRadius = cornerRadius,
                    topLeft = Offset(x, 0f),
                    size = Size(width, size.height),
                )
                x += width + spacingPx
            }
        }
    }

object PagerDotsMotionKeys {
    val indicatorWidth = MotionTestValueKey<Dp>("indicatorWidth")
}

private fun Modifier.pagerDotsSemantics(
@@ -173,7 +163,6 @@ private fun Modifier.pagerDotsSemantics(
    )
}

private fun PagerState.isOverscrolling(): Boolean {
    val position = currentPage + currentPageOffsetFraction
    return position < 0 || position > pageCount - 1
private object PagerDotsDefaults {
    const val SPRING_STIFFNESS = 1600f
}
+3 −0
Original line number Diff line number Diff line
@@ -36,6 +36,9 @@
    <!-- The number of columns in the QuickSettings -->
    <integer name="quick_settings_num_columns">2</integer>

    <!-- The number of columns in the Split Shade QuickSettings -->
    <integer name="quick_settings_split_shade_num_columns">6</integer>

    <!-- Notifications are sized to match the width of two (of 4) qs tiles in landscape. -->
    <bool name="config_skinnyNotifsInLandscape">false</bool>

+2 −2
Original line number Diff line number Diff line
@@ -191,8 +191,8 @@ private fun FooterBar(
        }
        PagerDots(
            pagerState = pagerState,
            activeColor = MaterialTheme.colorScheme.primary,
            nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
            activeColor = MaterialTheme.colorScheme.onSurfaceVariant,
            nonActiveColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .5f),
            modifier = Modifier.wrapContentWidth(),
        )
        Row(Modifier.weight(1f)) {