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
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user