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

Commit b7dfe500 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Optimize Compose QS recompositions (1/2)

This CL optimizes the Compose QS recompositions by:

1. Making sure that Modifier.bounceable() and Modifier.sysuiResTag() do
   not invalidate a Modifier chain whenever they are recomposed.
2. Initializing the power button by the current value it should have
   rather than null. This prevents the composition of the power button
   to happen on the 2nd or 3rd frame when transitioning from QQS to QS.
3. Ensuring that the brightness slider does not recompose once its icon
   is available.
4. Ensuring that the build number does not recompose on 2nd or 3rd frame
   by caching its last value accross recompositions in a StateFlow
   inside the associated interactor.
5. Annotating a bunch of stable generic classes with @Stable.

Bug: 389985793
Test: Manual, observed the difference of recompositions in Perfetto and
 go/hsv
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Change-Id: I55186bb14630f1a41999ecc2773f6bfcf64eb05a
parent e18a7dcf
Loading
Loading
Loading
Loading
+53 −3
Original line number Diff line number Diff line
@@ -17,13 +17,20 @@
package com.android.compose.animation

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import kotlin.math.roundToInt

/** A component that can bounce in one dimension, for instance when it is tapped. */
@Stable
interface Bounceable {
    val bounce: Dp
}
@@ -46,6 +53,7 @@ interface Bounceable {
 *   RTL layouts) side. This can be used for grids for which the last item does not align perfectly
 *   with the end of the grid.
 */
@Stable
fun Modifier.bounceable(
    bounceable: Bounceable,
    previousBounceable: Bounceable?,
@@ -53,7 +61,47 @@ fun Modifier.bounceable(
    orientation: Orientation,
    bounceEnd: Boolean = nextBounceable != null,
): Modifier {
    return layout { measurable, constraints ->
    return this then
        BounceableElement(bounceable, previousBounceable, nextBounceable, orientation, bounceEnd)
}

private data class BounceableElement(
    private val bounceable: Bounceable,
    private val previousBounceable: Bounceable?,
    private val nextBounceable: Bounceable?,
    private val orientation: Orientation,
    private val bounceEnd: Boolean,
) : ModifierNodeElement<BounceableNode>() {
    override fun create(): BounceableNode {
        return BounceableNode(
            bounceable,
            previousBounceable,
            nextBounceable,
            orientation,
            bounceEnd,
        )
    }

    override fun update(node: BounceableNode) {
        node.bounceable = bounceable
        node.previousBounceable = previousBounceable
        node.nextBounceable = nextBounceable
        node.orientation = orientation
        node.bounceEnd = bounceEnd
    }
}

private class BounceableNode(
    var bounceable: Bounceable,
    var previousBounceable: Bounceable?,
    var nextBounceable: Bounceable?,
    var orientation: Orientation,
    var bounceEnd: Boolean = nextBounceable != null,
) : Modifier.Node(), LayoutModifierNode {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
    ): MeasureResult {
        // The constraints in the orientation should be fixed, otherwise there is no way to know
        // what the size of our child node will be without this animation code.
        checkFixedSize(constraints, orientation)
@@ -61,10 +109,12 @@ fun Modifier.bounceable(
        var sizePrevious = 0f
        var sizeNext = 0f

        val previousBounceable = previousBounceable
        if (previousBounceable != null) {
            sizePrevious += bounceable.bounce.toPx() - previousBounceable.bounce.toPx()
        }

        val nextBounceable = nextBounceable
        if (nextBounceable != null) {
            sizeNext += bounceable.bounce.toPx() - nextBounceable.bounce.toPx()
        } else if (bounceEnd) {
@@ -84,7 +134,7 @@ fun Modifier.bounceable(
                // constraints, otherwise the parent will automatically center this node given the
                // size that it expects us to be. This allows us to then place the element where we
                // want it to be.
                layout(idleWidth, placeable.height) {
                return layout(idleWidth, placeable.height) {
                    placeable.placeRelative(-sizePrevious.roundToInt(), 0)
                }
            }
@@ -95,7 +145,7 @@ fun Modifier.bounceable(
                    constraints.copy(minHeight = animatedHeight, maxHeight = animatedHeight)

                val placeable = measurable.measure(animatedConstraints)
                layout(placeable.width, idleHeight) {
                return layout(placeable.width, idleHeight) {
                    placeable.placeRelative(0, -sizePrevious.roundToInt())
                }
            }
+7 −3
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@

package com.android.systemui.compose.modifiers

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
@@ -26,7 +26,11 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
 * Set a test tag on this node so that it is associated with [resId]. This node will then be
 * accessible by integration tests using `sysuiResSelector(resId)`.
 */
@OptIn(ExperimentalComposeUiApi::class)
@Stable
fun Modifier.sysuiResTag(resId: String): Modifier {
    return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId")
    // TODO(b/372412931): Only compose the semantics modifier once, at the root of the SystemUI
    // window.
    return this.then(TestTagAsResourceIdModifier).testTag("com.android.systemui:id/$resId")
}

private val TestTagAsResourceIdModifier = Modifier.semantics { testTagsAsResourceId = true }
+25 −16
Original line number Diff line number Diff line
@@ -141,7 +141,7 @@ fun FooterActions(
        mutableStateOf<FooterActionsForegroundServicesButtonViewModel?>(null)
    }
    var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) }
    var power by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) }
    var power by remember { mutableStateOf(viewModel.initialPower()) }

    LaunchedEffect(
        context,
@@ -218,23 +218,19 @@ fun FooterActions(
            }

            val useModifierBasedExpandable = remember { QSComposeFragment.isEnabled }
            security?.let { SecurityButton(it, useModifierBasedExpandable, Modifier.weight(1f)) }
            foregroundServices?.let { ForegroundServicesButton(it, useModifierBasedExpandable) }
            userSwitcher?.let {
            SecurityButton({ security }, useModifierBasedExpandable, Modifier.weight(1f))
            ForegroundServicesButton({ foregroundServices }, useModifierBasedExpandable)
            IconButton(
                    it,
                { userSwitcher },
                useModifierBasedExpandable,
                Modifier.sysuiResTag("multi_user_switch"),
            )
            }
            IconButton(
                viewModel.settings,
                { viewModel.settings },
                useModifierBasedExpandable,
                Modifier.sysuiResTag("settings_button_container"),
            )
            power?.let {
                IconButton(it, useModifierBasedExpandable, Modifier.sysuiResTag("pm_lite"))
            }
            IconButton({ power }, useModifierBasedExpandable, Modifier.sysuiResTag("pm_lite"))
        }
    }
}
@@ -242,10 +238,11 @@ fun FooterActions(
/** The security button. */
@Composable
private fun SecurityButton(
    model: FooterActionsSecurityButtonViewModel,
    model: () -> FooterActionsSecurityButtonViewModel?,
    useModifierBasedExpandable: Boolean,
    modifier: Modifier = Modifier,
) {
    val model = model() ?: return
    val onClick: ((Expandable) -> Unit)? =
        model.onClick?.let { onClick ->
            val context = LocalContext.current
@@ -265,9 +262,10 @@ private fun SecurityButton(
/** The foreground services button. */
@Composable
private fun RowScope.ForegroundServicesButton(
    model: FooterActionsForegroundServicesButtonViewModel,
    model: () -> FooterActionsForegroundServicesButtonViewModel?,
    useModifierBasedExpandable: Boolean,
) {
    val model = model() ?: return
    if (model.displayText) {
        TextButton(
            Icon.Resource(R.drawable.ic_info_outline, contentDescription = null),
@@ -288,6 +286,17 @@ private fun RowScope.ForegroundServicesButton(
    }
}

/** A button with an icon. */
@Composable
fun IconButton(
    model: () -> FooterActionsButtonViewModel?,
    useModifierBasedExpandable: Boolean,
    modifier: Modifier = Modifier,
) {
    val model = model() ?: return
    IconButton(model, useModifierBasedExpandable, modifier)
}

/** A button with an icon. */
@Composable
fun IconButton(
+6 −11
Original line number Diff line number Diff line
@@ -164,16 +164,11 @@ fun BrightnessSlider(

    val activeIconColor = colors.activeTickColor
    val inactiveIconColor = colors.inactiveTickColor
    val trackIcon: DrawScope.(Offset, Color, Float) -> Unit =
        remember(painter) {
    val trackIcon: DrawScope.(Offset, Color, Float) -> Unit = remember {
        { offset, color, alpha ->
            translate(offset.x + IconPadding.toPx(), offset.y) {
                with(painter) {
                        draw(
                            IconSize.toSize(),
                            colorFilter = ColorFilter.tint(color),
                            alpha = alpha,
                        )
                    draw(IconSize.toSize(), colorFilter = ColorFilter.tint(color), alpha = alpha)
                }
            }
        }
+2 −0
Original line number Diff line number Diff line
@@ -19,11 +19,13 @@ package com.android.systemui.common.shared.model
import android.annotation.AttrRes
import android.annotation.ColorInt
import android.annotation.ColorRes
import androidx.compose.runtime.Stable

/**
 * Models a color that can be either a specific [Color.Loaded] value or a resolvable theme
 * [Color.Attribute]
 */
@Stable
sealed interface Color {

    data class Loaded(@ColorInt val color: Int) : Color
Loading