| /* |
| * 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 |
| } |