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

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);
}