mirror of
https://github.com/JakeWharton/mosaic.git
synced 2025-11-01 20:20:19 +08:00
Add fill and wrapContent modifiers (#252)
This commit is contained in:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user