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

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

Introduce lambda-based modifiers

This CL introduces some lambda-based modifiers to change the alpha,
paddings or size of a Composable without triggering recomposition when
those values change. This is especially useful when animating these
values, which happens a lot in the SystemUI shade given that a lot of
components size/alpha is driven by touch gestures.

Test: Manual
Bug: 247473910
Change-Id: If63d302dcad5ad753b4ebf90ed239e598baa55c1
parent 3d389de3
Loading
Loading
Loading
Loading
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.compose.modifiers

import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.offset

// This file was mostly copy/pasted from by androidx.compose.foundation.layout.Padding.kt and
// contains modifiers with lambda parameters to change the padding of a Composable without
// triggering recomposition when the paddings change.
//
// These should be used instead of the traditional size modifiers when the size changes often, for
// instance when it is animated.
//
// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
// APIs.

/** @see androidx.compose.foundation.layout.padding */
fun Modifier.padding(
    start: Density.() -> Int = PaddingUnspecified,
    top: Density.() -> Int = PaddingUnspecified,
    end: Density.() -> Int = PaddingUnspecified,
    bottom: Density.() -> Int = PaddingUnspecified,
) =
    this.then(
        PaddingModifier(
            start,
            top,
            end,
            bottom,
            rtlAware = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = "padding"
                    properties["start"] = start
                    properties["top"] = top
                    properties["end"] = end
                    properties["bottom"] = bottom
                }
        )
    )

/** @see androidx.compose.foundation.layout.padding */
fun Modifier.padding(
    horizontal: Density.() -> Int = PaddingUnspecified,
    vertical: Density.() -> Int = PaddingUnspecified,
): Modifier {
    return this.then(
        PaddingModifier(
            start = horizontal,
            top = vertical,
            end = horizontal,
            bottom = vertical,
            rtlAware = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = "padding"
                    properties["horizontal"] = horizontal
                    properties["vertical"] = vertical
                }
        )
    )
}

private val PaddingUnspecified: Density.() -> Int = { 0 }

private class PaddingModifier(
    val start: Density.() -> Int,
    val top: Density.() -> Int,
    val end: Density.() -> Int,
    val bottom: Density.() -> Int,
    val rtlAware: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val start = start()
        val top = top()
        val end = end()
        val bottom = bottom()

        val horizontal = start + end
        val vertical = top + bottom

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start, top)
            } else {
                placeable.place(start, top)
            }
        }
    }

    override fun hashCode(): Int {
        var result = start.hashCode()
        result = 31 * result + top.hashCode()
        result = 31 * result + end.hashCode()
        result = 31 * result + bottom.hashCode()
        result = 31 * result + rtlAware.hashCode()
        return result
    }

    override fun equals(other: Any?): Boolean {
        val otherModifier = other as? PaddingModifier ?: return false
        return start == otherModifier.start &&
            top == otherModifier.top &&
            end == otherModifier.end &&
            bottom == otherModifier.bottom &&
            rtlAware == otherModifier.rtlAware
    }
}
+247 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.compose.modifiers

import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth

// This file was mostly copy pasted from androidx.compose.foundation.layout.Size.kt and contains
// modifiers with lambda parameters to change the (min/max) size of a Composable without triggering
// recomposition when the sizes change.
//
// These should be used instead of the traditional size modifiers when the size changes often, for
// instance when it is animated.
//
// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
// APIs.

/** @see androidx.compose.foundation.layout.width */
fun Modifier.width(width: Density.() -> Int) =
    this.then(
        SizeModifier(
            minWidth = width,
            maxWidth = width,
            enforceIncoming = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = "width"
                    value = width
                }
        )
    )

/** @see androidx.compose.foundation.layout.height */
fun Modifier.height(height: Density.() -> Int) =
    this.then(
        SizeModifier(
            minHeight = height,
            maxHeight = height,
            enforceIncoming = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = "height"
                    value = height
                }
        )
    )

/** @see androidx.compose.foundation.layout.size */
fun Modifier.size(width: Density.() -> Int, height: Density.() -> Int) =
    this.then(
        SizeModifier(
            minWidth = width,
            maxWidth = width,
            minHeight = height,
            maxHeight = height,
            enforceIncoming = true,
            inspectorInfo =
                debugInspectorInfo {
                    name = "size"
                    properties["width"] = width
                    properties["height"] = height
                }
        )
    )

private val SizeUnspecified: Density.() -> Int = { 0 }

private class SizeModifier(
    private val minWidth: Density.() -> Int = SizeUnspecified,
    private val minHeight: Density.() -> Int = SizeUnspecified,
    private val maxWidth: Density.() -> Int = SizeUnspecified,
    private val maxHeight: Density.() -> Int = SizeUnspecified,
    private val enforceIncoming: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    private val Density.targetConstraints: Constraints
        get() {
            val maxWidth =
                if (maxWidth != SizeUnspecified) {
                    maxWidth().coerceAtLeast(0)
                } else {
                    Constraints.Infinity
                }
            val maxHeight =
                if (maxHeight != SizeUnspecified) {
                    maxHeight().coerceAtLeast(0)
                } else {
                    Constraints.Infinity
                }
            val minWidth =
                if (minWidth != SizeUnspecified) {
                    minWidth().coerceAtMost(maxWidth).coerceAtLeast(0).let {
                        if (it != Constraints.Infinity) it else 0
                    }
                } else {
                    0
                }
            val minHeight =
                if (minHeight != SizeUnspecified) {
                    minHeight().coerceAtMost(maxHeight).coerceAtLeast(0).let {
                        if (it != Constraints.Infinity) it else 0
                    }
                } else {
                    0
                }
            return Constraints(
                minWidth = minWidth,
                minHeight = minHeight,
                maxWidth = maxWidth,
                maxHeight = maxHeight
            )
        }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val wrappedConstraints =
            targetConstraints.let { targetConstraints ->
                if (enforceIncoming) {
                    constraints.constrain(targetConstraints)
                } else {
                    val resolvedMinWidth =
                        if (minWidth != SizeUnspecified) {
                            targetConstraints.minWidth
                        } else {
                            constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
                        }
                    val resolvedMaxWidth =
                        if (maxWidth != SizeUnspecified) {
                            targetConstraints.maxWidth
                        } else {
                            constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
                        }
                    val resolvedMinHeight =
                        if (minHeight != SizeUnspecified) {
                            targetConstraints.minHeight
                        } else {
                            constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
                        }
                    val resolvedMaxHeight =
                        if (maxHeight != SizeUnspecified) {
                            targetConstraints.maxHeight
                        } else {
                            constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
                        }
                    Constraints(
                        resolvedMinWidth,
                        resolvedMaxWidth,
                        resolvedMinHeight,
                        resolvedMaxHeight
                    )
                }
            }
        val placeable = measurable.measure(wrappedConstraints)
        return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        val constraints = targetConstraints
        return if (constraints.hasFixedWidth) {
            constraints.maxWidth
        } else {
            constraints.constrainWidth(measurable.minIntrinsicWidth(height))
        }
    }

    override fun IntrinsicMeasureScope.minIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ): Int {
        val constraints = targetConstraints
        return if (constraints.hasFixedHeight) {
            constraints.maxHeight
        } else {
            constraints.constrainHeight(measurable.minIntrinsicHeight(width))
        }
    }

    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        val constraints = targetConstraints
        return if (constraints.hasFixedWidth) {
            constraints.maxWidth
        } else {
            constraints.constrainWidth(measurable.maxIntrinsicWidth(height))
        }
    }

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ): Int {
        val constraints = targetConstraints
        return if (constraints.hasFixedHeight) {
            constraints.maxHeight
        } else {
            constraints.constrainHeight(measurable.maxIntrinsicHeight(width))
        }
    }

    override fun equals(other: Any?): Boolean {
        if (other !is SizeModifier) return false
        return minWidth == other.minWidth &&
            minHeight == other.minHeight &&
            maxWidth == other.maxWidth &&
            maxHeight == other.maxHeight &&
            enforceIncoming == other.enforceIncoming
    }

    override fun hashCode() =
        (((((minWidth.hashCode() * 31 + minHeight.hashCode()) * 31) + maxWidth.hashCode()) * 31) +
            maxHeight.hashCode()) * 31
}