| /* |
| * 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. |
| */ |
| @file:Suppress("NOTHING_TO_INLINE") |
| |
| package androidx.compose.ui.unit |
| |
| import androidx.compose.Immutable |
| import androidx.compose.Stable |
| |
| /** |
| * Immutable constraints used for measuring child Layouts or [LayoutModifier]s. A parent layout |
| * can measure their children using the measure method on the corresponding [Measurable]s, |
| * method which takes the [Constraints] the child has to follow. A measured child is then |
| * responsible to choose for themselves and return a size which satisfies the set of [Constraints] |
| * received from their parent: |
| * - minWidth <= chosenWidth <= maxWidth |
| * - minHeight <= chosenHeight <= maxHeight |
| * The parent can then access the child chosen size on the resulting [Placeable]. The parent is |
| * responsible of defining a valid positioning of the children according to their sizes, so the |
| * parent needs to measure the children with appropriate [Constraints], such that whatever valid |
| * sizes children choose, they can be laid out in a way that also respects the parent's incoming |
| * [Constraints]. Note that different children can be measured with different [Constraints]. |
| * A child is allowed to choose a size that does not satisfy its constraints. However, when this |
| * happens, the parent will not read from the [Placeable] the real size of the child, but rather |
| * one that was coerced in the child's constraints; therefore, a parent can assume that its |
| * children will always respect the constraints in their layout algorithm. When this does not |
| * happen in reality, the position assigned to the child will be automatically offset to be centered |
| * on the space assigned by the parent under the assumption that constraints were respected. |
| * A set of [Constraints] can have infinite maxWidth and/or maxHeight. This is a trick often |
| * used by parents to ask their children for their preferred size: unbounded constraints force |
| * children whose default behavior is to fill the available space (always size to |
| * maxWidth/maxHeight) to have an opinion about their preferred size. Most commonly, when measured |
| * with unbounded [Constraints], these children will fallback to size themselves to wrap their |
| * content, instead of expanding to fill the available space (this is not always true |
| * as it depends on the child layout model, but is a common behavior for core layout components). |
| * |
| * [Constraints] uses a [Long] to represent four values, [minWidth], [minHeight], [maxWidth], |
| * and [maxHeight]. The range of the values varies to allow for at most 256K in one dimension. |
| * There are four possible maximum ranges, 13 bits/18 bits, and 15 bits/16 bits for either width |
| * or height, depending on the needs. For example, a width could range up to 18 bits |
| * and the height up to 13 bits. Alternatively, the width could range up to 16 bits and the height |
| * up to 15 bits. The height and width requirements can be reversed, with a height of up to 18 bits |
| * and width of 13 bits or height of 16 bits and width of 15 bits. Any constraints exceeding |
| * this range will fail. |
| */ |
| @Immutable |
| // This can be made inline after b/155690960 is fixed. |
| /*inline*/ data class Constraints( |
| @PublishedApi internal val value: Long |
| ) { |
| /** |
| * Indicates how the bits are assigned. One of: |
| * * MinFocusWidth |
| * * MaxFocusWidth |
| * * MinFocusHeight |
| * * MaxFocusHeight |
| */ |
| private val focusIndex |
| get() = (value and FocusMask).toInt() |
| |
| /** |
| * The minimum width that the measurement can take. |
| */ |
| val minWidth: Int |
| get() { |
| val mask = WidthMask[focusIndex] |
| return ((value shr 2).toInt() and mask) |
| } |
| |
| /** |
| * The maximum width that the measurement can take. This will either be |
| * a positive value greater than or equal to [minWidth] or [Constraints.Infinity]. |
| */ |
| val maxWidth: Int |
| get() { |
| val mask = WidthMask[focusIndex] |
| val width = ((value shr 33).toInt() and mask) |
| return if (width == 0) Infinity else width - 1 |
| } |
| |
| /** |
| * The minimum height that the measurement can take. |
| */ |
| val minHeight: Int |
| get() { |
| val focus = focusIndex |
| val mask = HeightMask[focus] |
| val offset = MinHeightOffsets[focus] |
| return (value shr offset).toInt() and mask |
| } |
| |
| /** |
| * The maximum height that the measurement can take. This will either be |
| * a positive value greater than or equal to [minHeight] or [Constraints.Infinity]. |
| */ |
| val maxHeight: Int |
| get() { |
| val focus = focusIndex |
| val mask = HeightMask[focus] |
| val offset = MinHeightOffsets[focus] + 31 |
| val height = (value shr offset).toInt() and mask |
| return if (height == 0) Infinity else height - 1 |
| } |
| |
| /** |
| * Whether or not the upper bound on the maximum width. |
| * @see hasBoundedHeight |
| */ |
| val hasBoundedWidth: Boolean |
| get() { |
| val mask = WidthMask[focusIndex] |
| return ((value shr 33).toInt() and mask) != 0 |
| } |
| |
| /** |
| * Whether or not the upper bound on the maximum height. |
| * @see hasBoundedWidth |
| */ |
| val hasBoundedHeight: Boolean |
| get() { |
| val focus = focusIndex |
| val mask = HeightMask[focus] |
| val offset = MinHeightOffsets[focus] + 31 |
| return ((value shr offset).toInt() and mask) != 0 |
| } |
| |
| /** |
| * Copies the existing [Constraints], replacing some of [minWidth], [minHeight], [maxWidth], |
| * or [maxHeight] as desired. [minWidth] and [minHeight] must be positive and |
| * [maxWidth] and [maxHeight] must be greater than or equal to [minWidth] and [minHeight], |
| * respectively, or [Infinity]. |
| */ |
| fun copy( |
| minWidth: Int = this.minWidth, |
| maxWidth: Int = this.maxWidth, |
| minHeight: Int = this.minHeight, |
| maxHeight: Int = this.maxHeight |
| ): Constraints { |
| require(minHeight >= 0 && minWidth >= 0) { |
| "minHeight($minHeight) and minWidth($minWidth) must be >= 0" |
| } |
| require(maxWidth >= minWidth || maxWidth == Infinity) { |
| "maxWidth($maxWidth) must be >= minWidth($minWidth)" |
| } |
| require(maxHeight >= minHeight || maxHeight == Infinity) { |
| "maxHeight($maxHeight) must be >= minHeight($minHeight)" |
| } |
| return createConstraints(minWidth, maxWidth, minHeight, maxHeight) |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (other !is Constraints) { |
| return false |
| } |
| return value == other.value |
| } |
| |
| override fun hashCode(): Int { |
| return value.hashCode() |
| } |
| |
| override fun toString(): String { |
| val maxWidth = maxWidth |
| val maxWidthStr = if (maxWidth == Infinity) "Infinity" else maxWidth.toString() |
| val maxHeight = maxHeight |
| val maxHeightStr = if (maxHeight == Infinity) "Infinity" else maxHeight.toString() |
| return "Constraints(minWidth = $minWidth, maxWidth = $maxWidthStr, " + |
| "minHeight = $minHeight, maxHeight = $maxHeightStr)" |
| } |
| |
| companion object { |
| /** |
| * A value that [maxWidth] or [maxHeight] will be set to when the constraint should |
| * be considered infinite. [hasBoundedHeight] or [hasBoundedWidth] will be |
| * `true` when [maxHeight] or [maxWidth] is [Infinity], respectively. |
| */ |
| const val Infinity = Int.MAX_VALUE |
| |
| /** |
| * The bit distribution when the focus of the bits should be on the width, but only |
| * a minimal difference in focus. |
| * |
| * 16 bits assigned to width, 15 bits assigned to height. |
| */ |
| private const val MinFocusWidth = 0x00L |
| |
| /** |
| * The bit distribution when the focus of the bits should be on the width, and a |
| * maximal number of bits assigned to the width. |
| * |
| * 18 bits assigned to width, 13 bits assigned to height. |
| */ |
| private const val MaxFocusWidth = 0x01L |
| |
| /** |
| * The bit distribution when the focus of the bits should be on the height, but only |
| * a minimal difference in focus. |
| * |
| * 15 bits assigned to width, 16 bits assigned to height. |
| */ |
| private const val MinFocusHeight = 0x02L |
| |
| /** |
| * The bit distribution when the focus of the bits should be on the height, and a |
| * a maximal number of bits assigned to the height. |
| * |
| * 13 bits assigned to width, 18 bits assigned to height. |
| */ |
| private const val MaxFocusHeight = 0x03L |
| |
| /** |
| * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth], |
| * [MinFocusHeight], [MaxFocusHeight]). |
| */ |
| private const val FocusMask = 0x03L |
| |
| /** |
| * The number of bits used for the focused dimension when there is minimal focus. |
| */ |
| private const val MinFocusBits = 16 |
| |
| /** |
| * The mask to use for the focused dimension when there is minimal focus. |
| */ |
| private const val MinFocusMask = 0xFFFF // 64K (16 bits) |
| |
| /** |
| * The number of bits used for the non-focused dimension when there is minimal focus. |
| */ |
| private const val MinNonFocusBits = 15 |
| |
| /** |
| * The mask to use for the non-focused dimension when there is minimal focus. |
| */ |
| private const val MinNonFocusMask = 0x7FFF // 32K (15 bits) |
| |
| /** |
| * The number of bits to use for the focused dimension when there is maximal focus. |
| */ |
| private const val MaxFocusBits = 18 |
| |
| /** |
| * The mask to use for the focused dimension when there is maximal focus. |
| */ |
| private const val MaxFocusMask = 0x3FFFF // 256K (18 bits) |
| |
| /** |
| * The number of bits to use for the non-focused dimension when there is maximal focus. |
| */ |
| private const val MaxNonFocusBits = 13 |
| |
| /** |
| * The mask to use for the non-focused dimension when there is maximal focus. |
| */ |
| private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits) |
| |
| /** |
| * Minimum Height shift offsets into Long value, indexed by FocusMask |
| * Max offsets are these + 31 |
| * Width offsets are always either 2 (min) or 33 (max) |
| */ |
| private val MinHeightOffsets = intArrayOf( |
| 18, // MinFocusWidth: 2 + 16 |
| 20, // MaxFocusWidth: 2 + 18 |
| 17, // MinFocusHeight: 2 + 15 |
| 15 // MaxFocusHeight: 2 + 13 |
| ) |
| |
| /** |
| * The mask to use for both minimum and maximum width. |
| */ |
| private val WidthMask = intArrayOf( |
| MinFocusMask, // MinFocusWidth (16 bits) |
| MaxFocusMask, // MaxFocusWidth (18 bits) |
| MinNonFocusMask, // MinFocusHeight (15 bits) |
| MaxNonFocusMask // MaxFocusHeight (13 bits) |
| ) |
| |
| /** |
| * The mask to use for both minimum and maximum height. |
| */ |
| private val HeightMask = intArrayOf( |
| MinNonFocusMask, // MinFocusWidth (15 bits) |
| MaxNonFocusMask, // MaxFocusWidth (13 bits) |
| MinFocusMask, // MinFocusHeight (16 bits) |
| MaxFocusMask // MaxFocusHeight (18 bits) |
| ) |
| |
| /** |
| * Creates constraints for fixed size in both dimensions. |
| */ |
| @Stable |
| fun fixed( |
| width: Int, |
| height: Int |
| ): Constraints { |
| require(width >= 0 && height >= 0) { |
| "width($width) and height($height) must be >= 0" |
| } |
| return createConstraints(width, width, height, height) |
| } |
| |
| /** |
| * Creates constraints for fixed width and unspecified height. |
| */ |
| @Stable |
| fun fixedWidth( |
| width: Int |
| ): Constraints { |
| require(width >= 0) { |
| "width($width) must be >= 0" |
| } |
| return createConstraints( |
| minWidth = width, |
| maxWidth = width, |
| minHeight = 0, |
| maxHeight = Infinity |
| ) |
| } |
| |
| /** |
| * Creates constraints for fixed height and unspecified width. |
| */ |
| @Stable |
| fun fixedHeight( |
| height: Int |
| ): Constraints { |
| require(height >= 0) { |
| "height($height) must be >= 0" |
| } |
| return createConstraints( |
| minWidth = 0, |
| maxWidth = Infinity, |
| minHeight = height, |
| maxHeight = height |
| ) |
| } |
| |
| /** |
| * Creates a [Constraints], only checking that the values fit in the packed Long. |
| */ |
| internal fun createConstraints( |
| minWidth: Int, |
| maxWidth: Int, |
| minHeight: Int, |
| maxHeight: Int |
| ): Constraints { |
| val heightVal = if (maxHeight == Infinity) minHeight else maxHeight |
| val heightBits = bitsNeedForSize(heightVal) |
| |
| val widthVal = if (maxWidth == Infinity) minWidth else maxWidth |
| val widthBits = bitsNeedForSize(widthVal) |
| |
| if (widthBits + heightBits > 31) { |
| throw IllegalArgumentException( |
| "Can't represent a width of $widthVal and height " + |
| "of $heightVal in Constraints" |
| ) |
| } |
| |
| val focus = when (widthBits) { |
| MinNonFocusBits -> MinFocusHeight |
| MinFocusBits -> MinFocusWidth |
| MaxNonFocusBits -> MaxFocusHeight |
| MaxFocusBits -> MaxFocusWidth |
| else -> throw IllegalStateException("Should only have the provided constants.") |
| } |
| |
| val maxWidthValue = if (maxWidth == Infinity) 0 else maxWidth + 1 |
| val maxHeightValue = if (maxHeight == Infinity) 0 else maxHeight + 1 |
| |
| val minHeightOffset = MinHeightOffsets[focus.toInt()] |
| val maxHeightOffset = minHeightOffset + 31 |
| |
| val value = focus or |
| (minWidth.toLong() shl 2) or |
| (maxWidthValue.toLong() shl 33) or |
| (minHeight.toLong() shl minHeightOffset) or |
| (maxHeightValue.toLong() shl maxHeightOffset) |
| return Constraints(value) |
| } |
| |
| private fun bitsNeedForSize(size: Int): Int { |
| return when { |
| size < MaxNonFocusMask -> MaxNonFocusBits |
| size < MinNonFocusMask -> MinNonFocusBits |
| size < MinFocusMask -> MinFocusBits |
| size < MaxFocusMask -> MaxFocusBits |
| else -> throw IllegalArgumentException( |
| "Can't represent a size of $size in " + |
| "Constraints" |
| ) |
| } |
| } |
| } |
| } |
| |
| /** |
| * Create a [Constraints]. [minWidth] and [minHeight] must be positive and |
| * [maxWidth] and [maxHeight] must be greater than or equal to [minWidth] and [minHeight], |
| * respectively, or [Infinity][Constraints.Infinity]. |
| */ |
| @Stable |
| fun Constraints( |
| minWidth: Int = 0, |
| maxWidth: Int = Constraints.Infinity, |
| minHeight: Int = 0, |
| maxHeight: Int = Constraints.Infinity |
| ): Constraints { |
| require(maxWidth >= minWidth) { |
| "maxWidth($maxWidth) must be >= than minWidth($minWidth)" |
| } |
| require(maxHeight >= minHeight) { |
| "maxHeight($maxHeight) must be >= than minHeight($minHeight)" |
| } |
| require(minWidth >= 0 && minHeight >= 0) { |
| "minWidth($minWidth) and minHeight($minHeight) must be >= 0" |
| } |
| return Constraints.createConstraints(minWidth, maxWidth, minHeight, maxHeight) |
| } |
| |
| /** |
| * Whether there is exactly one width value that satisfies the constraints. |
| */ |
| @Stable |
| val Constraints.hasFixedWidth get() = maxWidth == minWidth |
| |
| /** |
| * Whether there is exactly one height value that satisfies the constraints. |
| */ |
| @Stable |
| val Constraints.hasFixedHeight get() = maxHeight == minHeight |
| |
| /** |
| * Whether the area of a component respecting these constraints will definitely be 0. |
| * This is true when at least one of maxWidth and maxHeight are 0. |
| */ |
| @Stable |
| val Constraints.isZero get() = maxWidth == 0 || maxHeight == 0 |
| |
| /** |
| * Returns the result of coercing the current constraints in a different set of constraints. |
| */ |
| @Stable |
| fun Constraints.enforce(otherConstraints: Constraints) = Constraints( |
| minWidth = minWidth.coerceIn(otherConstraints.minWidth, otherConstraints.maxWidth), |
| maxWidth = maxWidth.coerceIn(otherConstraints.minWidth, otherConstraints.maxWidth), |
| minHeight = minHeight.coerceIn(otherConstraints.minHeight, otherConstraints.maxHeight), |
| maxHeight = maxHeight.coerceIn(otherConstraints.minHeight, otherConstraints.maxHeight) |
| ) |
| |
| /** |
| * Takes a size and returns the closest size to it that satisfies the constraints. |
| */ |
| @Stable |
| fun Constraints.constrain(size: IntSize) = IntSize( |
| width = size.width.coerceIn(minWidth, maxWidth), |
| height = size.height.coerceIn(minHeight, maxHeight) |
| ) |
| |
| /** |
| * Takes a width and returns the closest size to it that satisfies the constraints. |
| */ |
| @Stable |
| fun Constraints.constrainWidth(width: Int) = width.coerceIn(minWidth, maxWidth) |
| |
| /** |
| * Takes a height and returns the closest size to it that satisfies the constraints. |
| */ |
| @Stable |
| fun Constraints.constrainHeight(height: Int) = height.coerceIn(minHeight, maxHeight) |
| |
| /** |
| * Takes a size and returns whether it satisfies the current constraints. |
| */ |
| @Stable |
| fun Constraints.satisfiedBy(size: IntSize): Boolean { |
| return size.width in minWidth..maxWidth && size.height in minHeight..maxHeight |
| } |
| |
| /** |
| * Returns the Constraints obtained by offsetting the current instance with the given values. |
| */ |
| @Stable |
| fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints( |
| (minWidth + horizontal).coerceAtLeast(0), |
| addMaxWithMinimum(maxWidth, horizontal), |
| (minHeight + vertical).coerceAtLeast(0), |
| addMaxWithMinimum(maxHeight, vertical) |
| ) |
| |
| private fun addMaxWithMinimum(max: Int, value: Int): Int { |
| return if (max == Constraints.Infinity) { |
| max |
| } else { |
| (max + value).coerceAtLeast(0) |
| } |
| } |