Add fill and wrapContent modifiers (#252)

This commit is contained in:
EpicDima
2023-11-19 06:26:59 +03:00
committed by GitHub
parent e8e498e34f
commit d491a59894

View File

@ -2,11 +2,14 @@ package com.jakewharton.mosaic.layout
import androidx.compose.runtime.Stable
import com.jakewharton.mosaic.modifier.Modifier
import com.jakewharton.mosaic.ui.Alignment
import com.jakewharton.mosaic.ui.unit.Constraints
import com.jakewharton.mosaic.ui.unit.IntOffset
import com.jakewharton.mosaic.ui.unit.IntSize
import com.jakewharton.mosaic.ui.unit.constrain
import com.jakewharton.mosaic.ui.unit.constrainHeight
import com.jakewharton.mosaic.ui.unit.constrainWidth
import kotlin.math.roundToInt
/**
* Declare the preferred width of the content to be exactly [width]. The incoming measurement
@ -316,6 +319,155 @@ public fun Modifier.requiredSizeIn(
),
)
/**
* Have the content fill (possibly only partially) the [Constraints.maxWidth] of the incoming
* measurement constraints, by setting the [minimum width][Constraints.minWidth] and the
* [maximum width][Constraints.maxWidth] to be equal to the [maximum width][Constraints.maxWidth]
* multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
* make the content fill the whole available width. If the incoming maximum width is
* [Constraints.Infinity] this modifier will have no effect.
*
* @param fraction The fraction of the maximum width to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxWidth(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxWidth else FillModifier.width(fraction))
}
private val FillWholeMaxWidth = FillModifier.width(1f)
/**
* Have the content fill (possibly only partially) the [Constraints.maxHeight] of the incoming
* measurement constraints, by setting the [minimum height][Constraints.minHeight] and the
* [maximum height][Constraints.maxHeight] to be equal to the
* [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default,
* the [fraction] is 1, so the modifier will make the content fill the whole available height.
* If the incoming maximum height is [Constraints.Infinity] this modifier will have no effect.
*
* @param fraction The fraction of the maximum height to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxHeight(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxHeight else FillModifier.height(fraction))
}
private val FillWholeMaxHeight = FillModifier.height(1f)
/**
* Have the content fill (possibly only partially) the [Constraints.maxWidth] and
* [Constraints.maxHeight] of the incoming measurement constraints, by setting the
* [minimum width][Constraints.minWidth] and the [maximum width][Constraints.maxWidth] to be
* equal to the [maximum width][Constraints.maxWidth] multiplied by [fraction], as well as
* the [minimum height][Constraints.minHeight] and the [maximum height][Constraints.minHeight]
* to be equal to the [maximum height][Constraints.maxHeight] multiplied by [fraction].
* Note that, by default, the [fraction] is 1, so the modifier will make the content fill
* the whole available space.
* If the incoming maximum width or height is [Constraints.Infinity] this modifier will have no
* effect in that dimension.
*
* @param fraction The fraction of the maximum size to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxSize(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxSize else FillModifier.size(fraction))
}
private val FillWholeMaxSize = FillModifier.size(1f)
/**
* Allow the content to measure at its desired width without regard for the incoming measurement
* [minimum width constraint][Constraints.minWidth], and, if [unbounded] is true, also without
* regard for the incoming measurement [maximum width constraint][Constraints.maxWidth]. If
* the content's measured size is smaller than the minimum width constraint, [align]
* it within that minimum width space. If the content's measured size is larger than the maximum
* width constraint (only possible when [unbounded] is true), [align] over the maximum
* width space.
*/
@Stable
public fun Modifier.wrapContentWidth(
align: Alignment.Horizontal = Alignment.CenterHorizontally,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.CenterHorizontally && !unbounded) {
WrapContentWidthCenter
} else if (align == Alignment.Start && !unbounded) {
WrapContentWidthStart
} else {
WrapContentModifier.width(align, unbounded)
},
)
private val WrapContentWidthCenter =
WrapContentModifier.width(Alignment.CenterHorizontally, false)
private val WrapContentWidthStart = WrapContentModifier.width(Alignment.Start, false)
/**
* Allow the content to measure at its desired height without regard for the incoming measurement
* [minimum height constraint][Constraints.minHeight], and, if [unbounded] is true, also without
* regard for the incoming measurement [maximum height constraint][Constraints.maxHeight]. If the
* content's measured size is smaller than the minimum height constraint, [align] it within
* that minimum height space. If the content's measured size is larger than the maximum height
* constraint (only possible when [unbounded] is true), [align] over the maximum height space.
*/
@Stable
public fun Modifier.wrapContentHeight(
align: Alignment.Vertical = Alignment.CenterVertically,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.CenterVertically && !unbounded) {
WrapContentHeightCenter
} else if (align == Alignment.Top && !unbounded) {
WrapContentHeightTop
} else {
WrapContentModifier.height(align, unbounded)
},
)
private val WrapContentHeightCenter =
WrapContentModifier.height(Alignment.CenterVertically, false)
private val WrapContentHeightTop = WrapContentModifier.height(Alignment.Top, false)
/**
* Allow the content to measure at its desired size without regard for the incoming measurement
* [minimum width][Constraints.minWidth] or [minimum height][Constraints.minHeight] constraints,
* and, if [unbounded] is true, also without regard for the incoming maximum constraints.
* If the content's measured size is smaller than the minimum size constraint, [align] it
* within that minimum sized space. If the content's measured size is larger than the maximum
* size constraint (only possible when [unbounded] is true), [align] within the maximum space.
*/
@Stable
public fun Modifier.wrapContentSize(
align: Alignment = Alignment.Center,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.Center && !unbounded) {
WrapContentSizeCenter
} else if (align == Alignment.TopStart && !unbounded) {
WrapContentSizeTopStart
} else {
WrapContentModifier.size(align, unbounded)
},
)
private val WrapContentSizeCenter = WrapContentModifier.size(Alignment.Center, false)
private val WrapContentSizeTopStart = WrapContentModifier.size(Alignment.TopStart, false)
/**
* Constrain the size of the wrapped layout only when it would be otherwise unconstrained:
* the [minWidth] and [minHeight] constraints are only applied when the incoming corresponding
* constraint is `0`.
* The modifier can be used, for example, to define a default min size of a component,
* while still allowing it to be overidden with smaller min sizes across usages.
*/
@Stable
public fun Modifier.defaultMinSize(
minWidth: Int = Unspecified,
minHeight: Int = Unspecified,
): Modifier = this.then(UnspecifiedConstraintsModifier(minWidth = minWidth, minHeight = minHeight))
private class SizeModifier(
private val minWidth: Int = Unspecified,
private val minHeight: Int = Unspecified,
@ -467,4 +619,214 @@ private class SizeModifier(
}
}
private class FillModifier(
private val direction: Direction,
private val fraction: Float,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val minWidth: Int
val maxWidth: Int
if (constraints.hasBoundedWidth && direction != Direction.Vertical) {
val width = (constraints.maxWidth * fraction).roundToInt()
.coerceIn(constraints.minWidth, constraints.maxWidth)
minWidth = width
maxWidth = width
} else {
minWidth = constraints.minWidth
maxWidth = constraints.maxWidth
}
val minHeight: Int
val maxHeight: Int
if (constraints.hasBoundedHeight && direction != Direction.Horizontal) {
val height = (constraints.maxHeight * fraction).roundToInt()
.coerceIn(constraints.minHeight, constraints.maxHeight)
minHeight = height
maxHeight = height
} else {
minHeight = constraints.minHeight
maxHeight = constraints.maxHeight
}
val placeable = measurable.measure(
Constraints(minWidth, maxWidth, minHeight, maxHeight),
)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
override fun toString(): String = "Fill(direction=$direction, fraction=$fraction)"
companion object {
@Stable
fun width(fraction: Float) = FillModifier(
direction = Direction.Horizontal,
fraction = fraction,
)
@Stable
fun height(fraction: Float) = FillModifier(
direction = Direction.Vertical,
fraction = fraction,
)
@Stable
fun size(fraction: Float) = FillModifier(
direction = Direction.Both,
fraction = fraction,
)
}
}
private class WrapContentModifier(
private val direction: Direction,
private val unbounded: Boolean,
private val alignmentCallback: (IntSize) -> IntOffset,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val wrappedConstraints = Constraints(
minWidth = if (direction != Direction.Vertical) 0 else constraints.minWidth,
minHeight = if (direction != Direction.Horizontal) 0 else constraints.minHeight,
maxWidth = if (direction != Direction.Vertical && unbounded) {
Constraints.Infinity
} else {
constraints.maxWidth
},
maxHeight = if (direction != Direction.Horizontal && unbounded) {
Constraints.Infinity
} else {
constraints.maxHeight
},
)
val placeable = measurable.measure(wrappedConstraints)
val wrapperWidth = placeable.width.coerceIn(constraints.minWidth, constraints.maxWidth)
val wrapperHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
return layout(
wrapperWidth,
wrapperHeight,
) {
val position = alignmentCallback(
IntSize(wrapperWidth - placeable.width, wrapperHeight - placeable.height),
)
placeable.place(position)
}
}
override fun toString(): String = "WrapContent(direction=$direction, unbounded=$unbounded)"
companion object {
@Stable
fun width(
align: Alignment.Horizontal,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Horizontal,
unbounded = unbounded,
alignmentCallback = { size -> IntOffset(align.align(0, size.width), 0) },
)
@Stable
fun height(
align: Alignment.Vertical,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Vertical,
unbounded = unbounded,
alignmentCallback = { size -> IntOffset(0, align.align(0, size.height)) },
)
@Stable
fun size(
align: Alignment,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Both,
unbounded = unbounded,
alignmentCallback = { size -> align.align(IntSize.Zero, size) },
)
}
}
private class UnspecifiedConstraintsModifier(
private val minWidth: Int = Unspecified,
private val minHeight: Int = Unspecified,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val wrappedConstraints = Constraints(
if (minWidth != Unspecified && constraints.minWidth == 0) {
minWidth.coerceAtMost(constraints.maxWidth).coerceAtLeast(0)
} else {
constraints.minWidth
},
constraints.maxWidth,
if (minHeight != Unspecified && constraints.minHeight == 0) {
minHeight.coerceAtMost(constraints.maxHeight).coerceAtLeast(0)
} else {
constraints.minHeight
},
constraints.maxHeight,
)
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
override fun minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) = measurable.minIntrinsicWidth(height).coerceAtLeast(
if (minWidth != Unspecified) minWidth else 0,
)
override fun maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) = measurable.maxIntrinsicWidth(height).coerceAtLeast(
if (minWidth != Unspecified) minWidth else 0,
)
override fun minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) = measurable.minIntrinsicHeight(width).coerceAtLeast(
if (minHeight != Unspecified) minHeight else 0,
)
override fun maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) = measurable.maxIntrinsicHeight(width).coerceAtLeast(
if (minHeight != Unspecified) minHeight else 0,
)
override fun toString(): String {
val params = buildList {
if (minWidth != Unspecified) {
add("minW=$minWidth")
}
if (minHeight != Unspecified) {
add("minH=$minHeight")
}
}
return "UnspecifiedConstraints(${params.joinToString(", ")})"
}
}
internal enum class Direction {
Vertical,
Horizontal,
Both,
}
private const val Unspecified = Int.MIN_VALUE