blob: 767bb65e859aece9ba66ddc971132ae2d33f8477 [file]
/*
* Copyright 2019 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 androidx.compose.foundation.layout
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.internal.requirePrecondition
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastRoundToInt
/**
* Attempts to size the content to match a specified aspect ratio by trying to match one of the
* incoming constraints in the following order: [Constraints.maxWidth], [Constraints.maxHeight],
* [Constraints.minWidth], [Constraints.minHeight] if [matchHeightConstraintsFirst] is `false`
* (which is the default), or [Constraints.maxHeight], [Constraints.maxWidth],
* [Constraints.minHeight], [Constraints.minWidth] if [matchHeightConstraintsFirst] is `true`. The
* size in the other dimension is determined by the aspect ratio. The combinations will be tried in
* this order until one non-empty is found to satisfy the constraints. If no valid size is obtained
* this way, it means that there is no non-empty size satisfying both the constraints and the aspect
* ratio, so the constraints will not be respected and the content will be sized such that the
* [Constraints.maxWidth] or [Constraints.maxHeight] is matched (depending on
* [matchHeightConstraintsFirst]).
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.SimpleAspectRatio
* @param ratio the desired width/height positive ratio
* @param matchHeightConstraintsFirst if true, height constraints will be matched before width
* constraints and used to calculate the resulting size according to [ratio]
*/
@Stable
fun Modifier.aspectRatio(
@FloatRange(from = 0.0, fromInclusive = false) ratio: Float,
matchHeightConstraintsFirst: Boolean = false,
) =
this.then(
AspectRatioElement(
ratio,
matchHeightConstraintsFirst,
debugInspectorInfo {
name = "aspectRatio"
properties["ratio"] = ratio
properties["matchHeightConstraintsFirst"] = matchHeightConstraintsFirst
},
)
)
private class AspectRatioElement(
val aspectRatio: Float,
val matchHeightConstraintsFirst: Boolean,
val inspectorInfo: InspectorInfo.() -> Unit,
) : ModifierNodeElement<AspectRatioNode>() {
init {
requirePrecondition(aspectRatio > 0) { "aspectRatio $aspectRatio must be > 0" }
}
override fun create(): AspectRatioNode {
return AspectRatioNode(aspectRatio, matchHeightConstraintsFirst)
}
override fun update(node: AspectRatioNode) {
node.aspectRatio = aspectRatio
node.matchHeightConstraintsFirst = matchHeightConstraintsFirst
}
override fun InspectorInfo.inspectableProperties() {
inspectorInfo()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? AspectRatioElement ?: return false
return aspectRatio == otherModifier.aspectRatio &&
matchHeightConstraintsFirst == other.matchHeightConstraintsFirst
}
override fun hashCode(): Int =
aspectRatio.hashCode() * 31 + matchHeightConstraintsFirst.hashCode()
}
private class AspectRatioNode(var aspectRatio: Float, var matchHeightConstraintsFirst: Boolean) :
LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val size = constraints.findSize()
val wrappedConstraints =
if (size != IntSize.Zero) {
Constraints.fixed(size.width, size.height)
} else {
constraints
}
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) =
if (height != Constraints.Infinity) {
(height * aspectRatio).fastRoundToInt()
} else {
measurable.minIntrinsicWidth(height)
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) =
if (height != Constraints.Infinity) {
(height * aspectRatio).fastRoundToInt()
} else {
measurable.maxIntrinsicWidth(height)
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) =
if (width != Constraints.Infinity) {
(width / aspectRatio).fastRoundToInt()
} else {
measurable.minIntrinsicHeight(width)
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) =
if (width != Constraints.Infinity) {
(width / aspectRatio).fastRoundToInt()
} else {
measurable.maxIntrinsicHeight(width)
}
private fun Constraints.findSize(): IntSize {
if (!matchHeightConstraintsFirst) {
tryMaxWidth(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMaxHeight(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMinWidth(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMinHeight(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMaxWidth(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMaxHeight(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMinWidth(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMinHeight(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
} else {
tryMaxHeight(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMaxWidth(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMinHeight(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMinWidth(enforceConstraints = true).also { if (it != IntSize.Zero) return it }
tryMaxHeight(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMaxWidth(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMinHeight(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
tryMinWidth(enforceConstraints = false).also { if (it != IntSize.Zero) return it }
}
return IntSize.Zero
}
private fun Constraints.tryMaxWidth(enforceConstraints: Boolean): IntSize {
val maxWidth = this.maxWidth
if (maxWidth != Constraints.Infinity) {
val height = (maxWidth / aspectRatio).fastRoundToInt()
if (height > 0) {
if (!enforceConstraints || isSatisfiedBy(maxWidth, height)) {
return IntSize(maxWidth, height)
}
}
}
return IntSize.Zero
}
private fun Constraints.tryMaxHeight(enforceConstraints: Boolean): IntSize {
val maxHeight = this.maxHeight
if (maxHeight != Constraints.Infinity) {
val width = (maxHeight * aspectRatio).fastRoundToInt()
if (width > 0) {
if (!enforceConstraints || isSatisfiedBy(width, maxHeight)) {
return IntSize(width, maxHeight)
}
}
}
return IntSize.Zero
}
private fun Constraints.tryMinWidth(enforceConstraints: Boolean): IntSize {
val minWidth = this.minWidth
val height = (minWidth / aspectRatio).fastRoundToInt()
if (height > 0) {
if (!enforceConstraints || isSatisfiedBy(minWidth, height)) {
return IntSize(minWidth, height)
}
}
return IntSize.Zero
}
private fun Constraints.tryMinHeight(enforceConstraints: Boolean): IntSize {
val minHeight = this.minHeight
val width = (minHeight * aspectRatio).fastRoundToInt()
if (width > 0) {
if (!enforceConstraints || isSatisfiedBy(width, minHeight)) {
return IntSize(width, minHeight)
}
}
return IntSize.Zero
}
}
/** Takes a size and returns whether it satisfies the current constraints. */
@Stable
internal fun Constraints.isSatisfiedBy(width: Int, height: Int): Boolean {
return width in minWidth..maxWidth && height in minHeight..maxHeight
}