mirror of
https://github.com/JakeWharton/mosaic.git
synced 2025-11-02 13:00:09 +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 androidx.compose.runtime.Stable
|
||||||
import com.jakewharton.mosaic.modifier.Modifier
|
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.Constraints
|
||||||
|
import com.jakewharton.mosaic.ui.unit.IntOffset
|
||||||
import com.jakewharton.mosaic.ui.unit.IntSize
|
import com.jakewharton.mosaic.ui.unit.IntSize
|
||||||
import com.jakewharton.mosaic.ui.unit.constrain
|
import com.jakewharton.mosaic.ui.unit.constrain
|
||||||
import com.jakewharton.mosaic.ui.unit.constrainHeight
|
import com.jakewharton.mosaic.ui.unit.constrainHeight
|
||||||
import com.jakewharton.mosaic.ui.unit.constrainWidth
|
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
|
* 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 class SizeModifier(
|
||||||
private val minWidth: Int = Unspecified,
|
private val minWidth: Int = Unspecified,
|
||||||
private val minHeight: 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
|
private const val Unspecified = Int.MIN_VALUE
|
||||||
|
|||||||
Reference in New Issue
Block a user