/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.layout
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Used to specify the arrangement of the layout's children in [Row] or [Column] in the main axis
* direction (horizontal and vertical, respectively).
*/
@Immutable
@OptIn(InternalLayoutApi::class)
object Arrangement {
/**
* Used to specify the horizontal arrangement of the layout's children in a [Row].
*/
@InternalLayoutApi
interface Horizontal {
/**
* Spacing that should be added between any two adjacent layout children.
*/
val spacing get() = 0.dp
/**
* Horizontally places the layout children inside the [Row].
*
* @param totalSize Available space that can be occupied by the children.
* @param size An array of sizes of all children.
* @param layoutDirection A layout direction, left-to-right or right-to-left, of the parent
* layout that should be taken into account when determining positions of the children.
* @param density The current density.
* @param outPosition An array of the size of [size] that returns the calculated positions.
*/
fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
)
}
/**
* Used to specify the vertical arrangement of the layout's children in a [Column].
*/
@InternalLayoutApi
interface Vertical {
/**
* Spacing that should be added between any two adjacent layout children.
*/
val spacing get() = 0.dp
/**
* Vertically places the layout children inside the [Column].
*
* @param totalSize Available space that can be occupied by the children.
* @param size An array of sizes of all children.
* @param density The current density.
* @param outPosition An array of the size of [size] that returns the calculated positions.
*/
fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
)
}
/**
* Used to specify the horizontal arrangement of the layout's children in a [Row], or
* the vertical arrangement of the layout's children in a [Column].
*/
@InternalLayoutApi
interface HorizontalOrVertical : Horizontal, Vertical {
/**
* Spacing that should be added between any two adjacent layout children.
*/
override val spacing: Dp get() = 0.dp
}
/**
* Place children horizontally such that they are as close as possible to the beginning of the
* main axis.
*/
val Start = object : Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeLeftOrTop(size, outPosition)
} else {
size.reverse()
placeRightOrBottom(totalSize, size, outPosition)
outPosition.reverse()
}
}
/**
* Place children horizontally such that they are as close as possible to the end of the main
* axis.
*/
val End = object : Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeRightOrBottom(totalSize, size, outPosition)
} else {
size.reverse()
placeLeftOrTop(size, outPosition)
outPosition.reverse()
}
}
/**
* Place children vertically such that they are as close as possible to the top of the main
* axis.
*/
val Top = object : Vertical {
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeLeftOrTop(size, outPosition)
}
/**
* Place children vertically such that they are as close as possible to the bottom of the main
* axis.
*/
val Bottom = object : Vertical {
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeRightOrBottom(totalSize, size, outPosition)
}
/**
* Place children such that they are as close as possible to the middle of the main axis.
*/
val Center = object : HorizontalOrVertical {
override val spacing = 0.dp
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeCenter(totalSize, size, outPosition)
} else {
size.reverse()
placeCenter(totalSize, size, outPosition)
outPosition.reverse()
}
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeCenter(totalSize, size, outPosition)
}
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
*/
val SpaceEvenly = object : HorizontalOrVertical {
override val spacing = 0.dp
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeSpaceEvenly(totalSize, size, outPosition)
} else {
size.reverse()
placeSpaceEvenly(totalSize, size, outPosition)
outPosition.reverse()
}
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeSpaceEvenly(totalSize, size, outPosition)
}
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
*/
val SpaceBetween = object : HorizontalOrVertical {
override val spacing = 0.dp
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeSpaceBetween(totalSize, size, outPosition)
} else {
size.reverse()
placeSpaceBetween(totalSize, size, outPosition)
outPosition.reverse()
}
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeSpaceBetween(totalSize, size, outPosition)
}
/**
* 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.
*/
val SpaceAround = object : HorizontalOrVertical {
override val spacing = 0.dp
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeSpaceAround(totalSize, size, outPosition)
} else {
size.reverse()
placeSpaceAround(totalSize, size, outPosition)
outPosition.reverse()
}
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = placeSpaceAround(totalSize, size, outPosition)
}
/**
* Place children such that each two adjacent ones are spaced by a fixed [space] distance across
* the main axis. The spacing will be subtracted from the available space that the children
* can occupy.
*
* @param space The space between adjacent children.
*/
fun spacedBy(space: Dp): HorizontalOrVertical =
SpacedAligned(space, true, null)
/**
* Place children horizontally such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available width that the children
* can occupy. An [alignment] can be specified to align the spaced children horizontally
* inside the parent, in case there is empty width remaining.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(space, true) { size, layoutDirection ->
alignment.align(size, layoutDirection)
}
/**
* Place children vertically such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available height that the children
* can occupy. An [alignment] can be specified to align the spaced children vertically
* inside the parent, in case there is empty height remaining.
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical =
SpacedAligned(space, false) { size, _ -> alignment.align(size) }
/**
* Place children horizontally one next to the other and align the obtained group
* according to an [alignment].
*
* @param alignment The alignment of the children inside the parent.
*/
fun aligned(alignment: Alignment.Horizontal): Horizontal =
SpacedAligned(0.dp, true) { size, layoutDirection ->
alignment.align(size, layoutDirection)
}
/**
* Place children vertically one next to the other and align the obtained group
* according to an [alignment].
*
* @param alignment The alignment of the children inside the parent.
*/
fun aligned(alignment: Alignment.Vertical): Vertical =
SpacedAligned(0.dp, false) { size, _ -> alignment.align(size) }
/**
* Arrangement with spacing between adjacent children and alignment for the spaced group.
* Should not be instantiated directly, use [spacedBy] instead.
*/
internal data class SpacedAligned(
val space: Dp,
val rtlMirror: Boolean,
val alignment: ((Int, LayoutDirection) -> Int)?
) : HorizontalOrVertical {
override val spacing = space
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) {
if (size.isEmpty()) return
val spacePx = with(density) { space.toIntPx() }
var occupied = 0
var lastSpace = 0
if (layoutDirection == LayoutDirection.Rtl && rtlMirror) size.reverse()
size.forEachIndexed { index, it ->
outPosition[index] = min(occupied, totalSize - it)
lastSpace = min(spacePx, totalSize - outPosition[index] - it)
occupied = outPosition[index] + it + lastSpace
}
occupied -= lastSpace
if (alignment != null && occupied < totalSize) {
val groupPosition = alignment.invoke(totalSize - occupied, layoutDirection)
for (index in outPosition.indices) {
outPosition[index] += groupPosition
}
}
if (layoutDirection == LayoutDirection.Rtl && rtlMirror) outPosition.reverse()
}
override fun arrange(
totalSize: Int,
size: IntArray,
density: Density,
outPosition: IntArray
) = arrange(totalSize, size, LayoutDirection.Ltr, density, outPosition)
}
internal fun placeRightOrBottom(
totalSize: Int,
size: IntArray,
outPosition: IntArray
) {
val consumedSize = size.fold(0) { a, b -> a + b }
var current = totalSize - consumedSize
size.forEachIndexed { index, it ->
outPosition[index] = current
current += it
}
}
internal fun placeLeftOrTop(size: IntArray, outPosition: IntArray) {
var current = 0
size.forEachIndexed { index, it ->
outPosition[index] = current
current += it
}
}
internal fun placeCenter(totalSize: Int, size: IntArray, outPosition: IntArray) {
val consumedSize = size.fold(0) { a, b -> a + b }
var current = (totalSize - consumedSize).toFloat() / 2
size.forEachIndexed { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat()
}
}
internal fun placeSpaceEvenly(totalSize: Int, size: IntArray, outPosition: IntArray) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1)
var current = gapSize
size.forEachIndexed { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
internal fun placeSpaceBetween(totalSize: Int, size: IntArray, outPosition: IntArray) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = if (size.size > 1) {
(totalSize - consumedSize).toFloat() / (size.size - 1)
} else {
0f
}
var current = 0f
size.forEachIndexed { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
internal fun placeSpaceAround(totalSize: Int, size: IntArray, outPosition: IntArray) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = if (size.isNotEmpty()) {
(totalSize - consumedSize).toFloat() / size.size
} else {
0f
}
var current = gapSize / 2
size.forEachIndexed { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}
}
@Immutable
@OptIn(InternalLayoutApi::class)
object AbsoluteArrangement {
/**
* Place children horizontally such that they are as close as possible to the left edge of
* the [Row].
*
* Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val Left = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeLeftOrTop(size, outPosition)
}
/**
* Place children such that they are as close as possible to the middle of the [Row].
*
* Unlike [Arrangement.Center], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val Center = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeCenter(totalSize, size, outPosition)
}
/**
* Place children horizontally such that they are as close as possible to the right edge of
* the [Row].
*
* Unlike [Arrangement.End], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val Right = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeRightOrBottom(totalSize, size, outPosition)
}
/**
* Place children such that they are spaced evenly across the main axis, without free
* space before the first child or after the last child.
*
* Unlike [Arrangement.SpaceBetween], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val SpaceBetween = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeSpaceBetween(totalSize, size, outPosition)
}
/**
* Place children such that they are spaced evenly across the main axis, including free
* space before the first child and after the last child.
*
* Unlike [Arrangement.SpaceEvenly], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val SpaceEvenly = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeSpaceEvenly(totalSize, size, outPosition)
}
/**
* Place children such that they are spaced evenly horizontally, including free
* space before the first child and after the last child, but half the amount of space
* existing otherwise between two consecutive children.
*
* Unlike [Arrangement.SpaceAround], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*/
val SpaceAround = object : Arrangement.Horizontal {
override fun arrange(
totalSize: Int,
size: IntArray,
layoutDirection: LayoutDirection,
density: Density,
outPosition: IntArray
) = Arrangement.placeSpaceAround(totalSize, size, outPosition)
}
/**
* Place children such that each two adjacent ones are spaced by a fixed [space] distance across
* the main axis. The spacing will be subtracted from the available space that the children
* can occupy.
*
* Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*
* @param space The space between adjacent children.
*/
fun spacedBy(space: Dp): Arrangement.HorizontalOrVertical =
Arrangement.SpacedAligned(space, false, null)
/**
* Place children horizontally such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available width that the children
* can occupy. An [alignment] can be specified to align the spaced children horizontally
* inside the parent, in case there is empty width remaining.
*
* Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Arrangement.Horizontal =
Arrangement.SpacedAligned(space, false) { size, layoutDirection ->
alignment.align(size, layoutDirection)
}
/**
* Place children vertically such that each two adjacent ones are spaced by a fixed [space]
* distance. The spacing will be subtracted from the available height that the children
* can occupy. An [alignment] can be specified to align the spaced children vertically
* inside the parent, in case there is empty height remaining.
*
* Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*
* @param space The space between adjacent children.
* @param alignment The alignment of the spaced children inside the parent.
*/
fun spacedBy(space: Dp, alignment: Alignment.Vertical): Arrangement.Vertical =
Arrangement.SpacedAligned(space, false) { size, _ -> alignment.align(size) }
/**
* Place children horizontally one next to the other and align the obtained group
* according to an [alignment].
*
* Unlike [Arrangement.aligned], when the layout direction is RTL, the children will not be
* mirrored and as such children will appear in the order they are composed inside the [Row].
*
* @param alignment The alignment of the children inside the parent.
*/
fun aligned(alignment: Alignment.Horizontal): Arrangement.Horizontal =
Arrangement.SpacedAligned(0.dp, false) { size, layoutDirection ->
alignment.align(size, layoutDirection)
}
}