AlignmentLine.kt
/*
* Copyright 2019 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.Stable
import androidx.compose.ui.AlignmentLine
import androidx.compose.ui.HorizontalAlignmentLine
import androidx.compose.ui.LayoutModifier
import androidx.compose.ui.Measurable
import androidx.compose.ui.MeasureScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import kotlin.math.max
/**
* A [Modifier] that can add padding to position the content according to specified distances
* from its bounds to an [alignment line][AlignmentLine]. Whether the positioning is vertical
* or horizontal is defined by the orientation of the given [alignmentLine] (if the line is
* horizontal, [before] and [after] will refer to distances from top and bottom, otherwise they
* will refer to distances from start and end). The opposite axis sizing and positioning will
* remain unaffected.
* The modified layout will try to include the required padding, subject to the incoming max
* layout constraints, such that the distance from its bounds to the [alignmentLine] of the
* content will be [before] and [after], respectively. When the max constraints do not allow
* this, satisfying the [before] requirement will have priority over [after]. When the modified
* layout is min constrained in the affected layout direction and the padded layout is smaller
* than the constraint, the modified layout will satisfy the min constraint and the content will
* be positioned to satisfy the [before] requirement if specified, or the [after] requirement
* otherwise.
*
* @param alignmentLine the alignment line relative to which the padding is defined
* @param before the distance between the container's top edge and the horizontal alignment line, or
* the container's start edge and the vertical alignment line
* @param after the distance between the container's bottom edge and the horizontal alignment line,
* or the container's end edge and the vertical alignment line
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.RelativePaddingFromSample
*/
@Stable
fun Modifier.relativePaddingFrom(
alignmentLine: AlignmentLine,
before: Dp = Dp.Unspecified,
after: Dp = Dp.Unspecified
): Modifier = this.then(AlignmentLineOffset(alignmentLine, before, after))
private data class AlignmentLineOffset(
val alignmentLine: AlignmentLine,
val before: Dp,
val after: Dp
) : LayoutModifier {
init {
require(
(before.value >= 0f || before == Dp.Unspecified) &&
(after.value >= 0f || after == Dp.Unspecified)
) {
"Padding from alignment line must be a non-negative number"
}
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureScope.MeasureResult {
val placeable = measurable.measure(
// Loose constraints perpendicular on the alignment line.
if (alignmentLine.horizontal) constraints.copy(minHeight = 0)
else constraints.copy(minWidth = 0)
)
val linePosition = placeable[alignmentLine].let {
if (it != AlignmentLine.Unspecified) it else 0
}
val axis = if (alignmentLine.horizontal) placeable.height else placeable.width
val axisMax = if (alignmentLine.horizontal) constraints.maxHeight else constraints.maxWidth
// Compute padding required to satisfy the total before and after offsets.
val paddingBefore =
((if (before != Dp.Unspecified) before.toIntPx() else 0) - linePosition)
.coerceIn(0, axisMax - axis)
val paddingAfter =
((if (after != Dp.Unspecified) after.toIntPx() else 0) - axis + linePosition)
.coerceIn(0, axisMax - axis - paddingBefore)
val width = if (alignmentLine.horizontal) {
placeable.width
} else {
max(paddingBefore + placeable.width + paddingAfter, constraints.minWidth)
}
val height = if (alignmentLine.horizontal) {
max(paddingBefore + placeable.height + paddingAfter, constraints.minHeight)
} else {
placeable.height
}
return layout(width, height) {
val x = when {
alignmentLine.horizontal -> 0
before != Dp.Unspecified -> paddingBefore
else -> width - paddingAfter - placeable.width
}
val y = when {
!alignmentLine.horizontal -> 0
before != Dp.Unspecified -> paddingBefore
else -> height - paddingAfter - placeable.height
}
placeable.placeRelative(x, y)
}
}
}
private val AlignmentLine.horizontal: Boolean get() = this is HorizontalAlignmentLine