You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
13 KiB
337 lines
13 KiB
package com.example.jetsnack.flowlayout |
|
|
|
|
|
import androidx.compose.foundation.layout.Arrangement |
|
import androidx.compose.runtime.Composable |
|
import androidx.compose.ui.Alignment |
|
import androidx.compose.ui.Modifier |
|
import androidx.compose.ui.layout.Layout |
|
import androidx.compose.ui.layout.Placeable |
|
import androidx.compose.ui.unit.Constraints |
|
import androidx.compose.ui.unit.Dp |
|
import androidx.compose.ui.unit.IntSize |
|
import androidx.compose.ui.unit.LayoutDirection |
|
import androidx.compose.ui.unit.dp |
|
import kotlin.math.max |
|
|
|
internal enum class LayoutOrientation { |
|
Horizontal, |
|
Vertical |
|
} |
|
|
|
internal data class OrientationIndependentConstraints( |
|
val mainAxisMin: Int, |
|
val mainAxisMax: Int, |
|
val crossAxisMin: Int, |
|
val crossAxisMax: Int |
|
) { |
|
constructor(c: Constraints, orientation: LayoutOrientation) : this( |
|
if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight, |
|
if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight, |
|
if (orientation === LayoutOrientation.Horizontal) c.minHeight else c.minWidth, |
|
if (orientation === LayoutOrientation.Horizontal) c.maxHeight else c.maxWidth |
|
) |
|
} |
|
|
|
/** |
|
* A composable that places its children in a horizontal flow. Unlike [Row], if the |
|
* horizontal space is too small to put all the children in one row, multiple rows may be used. |
|
* |
|
* Note that just like [Row], flex values cannot be used with [FlowRow]. |
|
* |
|
* @param modifier The modifier to be applied to the FlowRow. |
|
* @param mainAxisSize The size of the layout in the main axis direction. |
|
* @param mainAxisAlignment The alignment of each row's children in the main axis direction. |
|
* @param mainAxisSpacing The main axis spacing between the children of each row. |
|
* @param crossAxisAlignment The alignment of each row's children in the cross axis direction. |
|
* @param crossAxisSpacing The cross axis spacing between the rows of the layout. |
|
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. |
|
*/ |
|
@Composable |
|
public fun FlowRow( |
|
modifier: Modifier = Modifier, |
|
mainAxisSize: SizeMode = SizeMode.Wrap, |
|
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, |
|
mainAxisSpacing: Dp = 0.dp, |
|
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, |
|
crossAxisSpacing: Dp = 0.dp, |
|
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, |
|
content: @Composable () -> Unit |
|
) { |
|
Flow( |
|
modifier = modifier, |
|
orientation = LayoutOrientation.Horizontal, |
|
mainAxisSize = mainAxisSize, |
|
mainAxisAlignment = mainAxisAlignment, |
|
mainAxisSpacing = mainAxisSpacing, |
|
crossAxisAlignment = crossAxisAlignment, |
|
crossAxisSpacing = crossAxisSpacing, |
|
lastLineMainAxisAlignment = lastLineMainAxisAlignment, |
|
content = content |
|
) |
|
} |
|
|
|
/** |
|
* A composable that places its children in a vertical flow. Unlike [Column], if the |
|
* vertical space is too small to put all the children in one column, multiple columns may be used. |
|
* |
|
* Note that just like [Column], flex values cannot be used with [FlowColumn]. |
|
* |
|
* @param modifier The modifier to be applied to the FlowColumn. |
|
* @param mainAxisSize The size of the layout in the main axis direction. |
|
* @param mainAxisAlignment The alignment of each column's children in the main axis direction. |
|
* @param mainAxisSpacing The main axis spacing between the children of each column. |
|
* @param crossAxisAlignment The alignment of each column's children in the cross axis direction. |
|
* @param crossAxisSpacing The cross axis spacing between the columns of the layout. |
|
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. |
|
*/ |
|
@Composable |
|
public fun FlowColumn( |
|
modifier: Modifier = Modifier, |
|
mainAxisSize: SizeMode = SizeMode.Wrap, |
|
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start, |
|
mainAxisSpacing: Dp = 0.dp, |
|
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start, |
|
crossAxisSpacing: Dp = 0.dp, |
|
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment, |
|
content: @Composable () -> Unit |
|
) { |
|
Flow( |
|
modifier = modifier, |
|
orientation = LayoutOrientation.Vertical, |
|
mainAxisSize = mainAxisSize, |
|
mainAxisAlignment = mainAxisAlignment, |
|
mainAxisSpacing = mainAxisSpacing, |
|
crossAxisAlignment = crossAxisAlignment, |
|
crossAxisSpacing = crossAxisSpacing, |
|
lastLineMainAxisAlignment = lastLineMainAxisAlignment, |
|
content = content |
|
) |
|
} |
|
|
|
/** |
|
* Used to specify the alignment of a layout's children, in cross axis direction. |
|
*/ |
|
public enum class FlowCrossAxisAlignment { |
|
/** |
|
* Place children such that their center is in the middle of the cross axis. |
|
*/ |
|
Center, |
|
/** |
|
* Place children such that their start edge is aligned to the start edge of the cross axis. |
|
*/ |
|
Start, |
|
/** |
|
* Place children such that their end edge is aligned to the end edge of the cross axis. |
|
*/ |
|
End, |
|
} |
|
|
|
public typealias FlowMainAxisAlignment = MainAxisAlignment |
|
|
|
/** |
|
* Layout model that arranges its children in a horizontal or vertical flow. |
|
*/ |
|
@Composable |
|
private fun Flow( |
|
modifier: Modifier, |
|
orientation: LayoutOrientation, |
|
mainAxisSize: SizeMode, |
|
mainAxisAlignment: FlowMainAxisAlignment, |
|
mainAxisSpacing: Dp, |
|
crossAxisAlignment: FlowCrossAxisAlignment, |
|
crossAxisSpacing: Dp, |
|
lastLineMainAxisAlignment: FlowMainAxisAlignment, |
|
content: @Composable () -> Unit |
|
) { |
|
fun Placeable.mainAxisSize() = |
|
if (orientation == LayoutOrientation.Horizontal) width else height |
|
fun Placeable.crossAxisSize() = |
|
if (orientation == LayoutOrientation.Horizontal) height else width |
|
|
|
Layout(content, modifier) { measurables, outerConstraints -> |
|
val sequences = mutableListOf<List<Placeable>>() |
|
val crossAxisSizes = mutableListOf<Int>() |
|
val crossAxisPositions = mutableListOf<Int>() |
|
|
|
var mainAxisSpace = 0 |
|
var crossAxisSpace = 0 |
|
|
|
val currentSequence = mutableListOf<Placeable>() |
|
var currentMainAxisSize = 0 |
|
var currentCrossAxisSize = 0 |
|
|
|
val constraints = OrientationIndependentConstraints(outerConstraints, orientation) |
|
|
|
val childConstraints = if (orientation == LayoutOrientation.Horizontal) { |
|
Constraints(maxWidth = constraints.mainAxisMax) |
|
} else { |
|
Constraints(maxHeight = constraints.mainAxisMax) |
|
} |
|
|
|
// Return whether the placeable can be added to the current sequence. |
|
fun canAddToCurrentSequence(placeable: Placeable) = |
|
currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + |
|
placeable.mainAxisSize() <= constraints.mainAxisMax |
|
|
|
// Store current sequence information and start a new sequence. |
|
fun startNewSequence() { |
|
if (sequences.isNotEmpty()) { |
|
crossAxisSpace += crossAxisSpacing.roundToPx() |
|
} |
|
sequences += currentSequence.toList() |
|
crossAxisSizes += currentCrossAxisSize |
|
crossAxisPositions += crossAxisSpace |
|
|
|
crossAxisSpace += currentCrossAxisSize |
|
mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) |
|
|
|
currentSequence.clear() |
|
currentMainAxisSize = 0 |
|
currentCrossAxisSize = 0 |
|
} |
|
|
|
for (measurable in measurables) { |
|
// Ask the child for its preferred size. |
|
val placeable = measurable.measure(childConstraints) |
|
|
|
// Start a new sequence if there is not enough space. |
|
if (!canAddToCurrentSequence(placeable)) startNewSequence() |
|
|
|
// Add the child to the current sequence. |
|
if (currentSequence.isNotEmpty()) { |
|
currentMainAxisSize += mainAxisSpacing.roundToPx() |
|
} |
|
currentSequence.add(placeable) |
|
currentMainAxisSize += placeable.mainAxisSize() |
|
currentCrossAxisSize = max(currentCrossAxisSize, placeable.crossAxisSize()) |
|
} |
|
|
|
if (currentSequence.isNotEmpty()) startNewSequence() |
|
|
|
val mainAxisLayoutSize = if (constraints.mainAxisMax != Constraints.Infinity && |
|
mainAxisSize == SizeMode.Expand |
|
) { |
|
constraints.mainAxisMax |
|
} else { |
|
max(mainAxisSpace, constraints.mainAxisMin) |
|
} |
|
val crossAxisLayoutSize = max(crossAxisSpace, constraints.crossAxisMin) |
|
|
|
val layoutWidth = if (orientation == LayoutOrientation.Horizontal) { |
|
mainAxisLayoutSize |
|
} else { |
|
crossAxisLayoutSize |
|
} |
|
val layoutHeight = if (orientation == LayoutOrientation.Horizontal) { |
|
crossAxisLayoutSize |
|
} else { |
|
mainAxisLayoutSize |
|
} |
|
|
|
layout(layoutWidth, layoutHeight) { |
|
sequences.forEachIndexed { i, placeables -> |
|
val childrenMainAxisSizes = IntArray(placeables.size) { j -> |
|
placeables[j].mainAxisSize() + |
|
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 |
|
} |
|
val arrangement = if (i < sequences.lastIndex) { |
|
mainAxisAlignment.arrangement |
|
} else { |
|
lastLineMainAxisAlignment.arrangement |
|
} |
|
// TODO(soboleva): rtl support |
|
// Handle vertical direction |
|
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } |
|
with(arrangement) { |
|
arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) |
|
} |
|
placeables.forEachIndexed { j, placeable -> |
|
val crossAxis = when (crossAxisAlignment) { |
|
FlowCrossAxisAlignment.Start -> 0 |
|
FlowCrossAxisAlignment.End -> |
|
crossAxisSizes[i] - placeable.crossAxisSize() |
|
FlowCrossAxisAlignment.Center -> |
|
Alignment.Center.align( |
|
IntSize.Zero, |
|
IntSize( |
|
width = 0, |
|
height = crossAxisSizes[i] - placeable.crossAxisSize() |
|
), |
|
LayoutDirection.Ltr |
|
).y |
|
} |
|
if (orientation == LayoutOrientation.Horizontal) { |
|
placeable.place( |
|
x = mainAxisPositions[j], |
|
y = crossAxisPositions[i] + crossAxis |
|
) |
|
} else { |
|
placeable.place( |
|
x = crossAxisPositions[i] + crossAxis, |
|
y = mainAxisPositions[j] |
|
) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Used to specify how a layout chooses its own size when multiple behaviors are possible. |
|
*/ |
|
// TODO(popam): remove this when Flow is reworked |
|
public enum class SizeMode { |
|
/** |
|
* Minimize the amount of free space by wrapping the children, |
|
* subject to the incoming layout constraints. |
|
*/ |
|
Wrap, |
|
/** |
|
* Maximize the amount of free space by expanding to fill the available space, |
|
* subject to the incoming layout constraints. |
|
*/ |
|
Expand |
|
} |
|
|
|
/** |
|
* Used to specify the alignment of a layout's children, in main axis direction. |
|
*/ |
|
public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { |
|
// TODO(soboleva) support RTl in Flow |
|
// workaround for now - use Arrangement that equals to previous Arrangement |
|
/** |
|
* Place children such that they are as close as possible to the middle of the main axis. |
|
*/ |
|
Center(Arrangement.Center), |
|
|
|
/** |
|
* Place children such that they are as close as possible to the start of the main axis. |
|
*/ |
|
Start(Arrangement.Top), |
|
|
|
/** |
|
* Place children such that they are as close as possible to the end of the main axis. |
|
*/ |
|
End(Arrangement.Bottom), |
|
|
|
/** |
|
* Place children such that they are spaced evenly across the main axis, including free |
|
* space before the first child and after the last child. |
|
*/ |
|
SpaceEvenly(Arrangement.SpaceEvenly), |
|
|
|
/** |
|
* Place children such that they are spaced evenly across the main axis, without free |
|
* space before the first child or after the last child. |
|
*/ |
|
SpaceBetween(Arrangement.SpaceBetween), |
|
|
|
/** |
|
* Place children such that they are spaced evenly across the main axis, including free |
|
* space before the first child and after the last child, but half the amount of space |
|
* existing otherwise between two consecutive children. |
|
*/ |
|
SpaceAround(Arrangement.SpaceAround); |
|
} |