[go: nahoru, domu]

blob: abdfe3dbd2727e920ddd74fd1b3ff83345ce7239 [file] [log] [blame]
/*
* 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.
*/
@file:Suppress("Deprecation")
package androidx.ui.layout
import android.annotation.SuppressLint
import android.util.Log
import androidx.annotation.FloatRange
import androidx.compose.Composable
import androidx.compose.remember
import androidx.constraintlayout.core.state.ConstraintReference
import androidx.constraintlayout.core.state.Dimension.SPREAD_DIMENSION
import androidx.constraintlayout.core.state.Dimension.WRAP_DIMENSION
import androidx.constraintlayout.core.state.helpers.BarrierReference
import androidx.constraintlayout.core.widgets.ConstraintWidget
import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.WRAP_CONTENT
import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.FIXED
import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_SPREAD
import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_WRAP
import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer
import androidx.constraintlayout.core.widgets.Optimizer
import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure
import androidx.compose.Immutable
import androidx.ui.core.Constraints
import androidx.ui.core.LayoutDirection
import androidx.ui.text.FirstBaseline
import androidx.ui.core.Measurable
import androidx.ui.core.MeasureScope
import androidx.ui.core.Modifier
import androidx.ui.core.MultiMeasureLayout
import androidx.ui.core.ParentDataModifier
import androidx.ui.core.Placeable
import androidx.ui.core.hasFixedHeight
import androidx.ui.core.hasFixedWidth
import androidx.ui.core.tag
import androidx.ui.layout.ConstraintSetBuilderScope.Companion.baselineAnchorFunction
import androidx.ui.layout.ConstraintSetBuilderScope.Companion.horizontalAnchorFunctions
import androidx.ui.layout.ConstraintSetBuilderScope.Companion.verticalAnchorFunctions
import androidx.ui.layout.ConstraintSetBuilderScope.Companion.verticalAnchorIndexToFunctionIndex
import androidx.ui.unit.Density
import androidx.ui.unit.Dp
import androidx.ui.unit.IntPx
import androidx.ui.unit.IntPxSize
import androidx.ui.unit.dp
import androidx.ui.unit.ipx
import androidx.ui.util.fastForEach
/**
* Layout that positions its children according to the constraints between them.
*
* Example usage:
* @sample androidx.ui.layout.samples.DemoInlineDSL
*/
@Composable
fun ConstraintLayout(
modifier: Modifier = Modifier,
children: @Composable ConstraintLayoutScope.() -> Unit
) {
val measurer = remember { Measurer() }
val scope = remember { ConstraintLayoutScope() }
@Suppress("Deprecation")
MultiMeasureLayout(modifier = modifier, children = {
scope.reset()
scope.children()
}) { measurables, constraints, layoutDirection ->
val constraintSet = object : ConstraintSet {
override fun applyTo(state: State, measurables: List<Measurable>) {
scope.applyTo(state)
measurables.fastForEach { measurable ->
val parentData = measurable.parentData as? ConstraintLayoutParentData
// Map the tag and the measurable, to be retrieved later during measurement.
val givenTag = parentData?.ref?.tag
state.map(givenTag ?: createTag(), measurable)
// Run the constrainAs block of the child, to obtain its constraints.
if (parentData != null) {
val constrainScope = ConstrainScope(parentData.ref.tag)
parentData.constrain(constrainScope)
constrainScope.applyTo(state)
}
}
}
}
val layoutSize = measurer.performMeasure(
constraints,
layoutDirection,
constraintSet,
measurables,
this
)
layout(layoutSize.width, layoutSize.height) {
with(measurer) { performLayout() }
}
}
}
/**
* Layout that positions its children according to the constraints between them.
*
* Example usage:
* @sample androidx.ui.layout.samples.DemoConstraintSet
*/
@Composable
fun ConstraintLayout(
constraintSet: ConstraintSet,
modifier: Modifier = Modifier,
children: @Composable () -> Unit
) {
val measurer = remember { Measurer() }
@Suppress("Deprecation")
MultiMeasureLayout(modifier, children) { measurables, constraints, layoutDirection ->
val layoutSize = measurer.performMeasure(
constraints,
layoutDirection,
constraintSet,
measurables,
this
)
layout(layoutSize.width, layoutSize.height) {
with(measurer) { performLayout() }
}
}
}
/**
* Represents a layout within a [ConstraintLayout].
*/
class ConstrainedLayoutReference(val tag: Any) {
/**
* The start anchor of this layout. Represents left in LTR layout direction, or right in RTL.
*/
val start = ConstraintLayoutBaseScope.VerticalAnchor(tag, -2)
/**
* The left anchor of this layout.
*/
val absoluteLeft = ConstraintLayoutBaseScope.VerticalAnchor(tag, 0)
/**
* The top anchor of this layout.
*/
val top = ConstraintLayoutBaseScope.HorizontalAnchor(tag, 0)
/**
* The end anchor of this layout. Represents right in LTR layout direction, or left in RTL.
*/
val end = ConstraintLayoutBaseScope.VerticalAnchor(tag, -1)
/**
* The right anchor of this layout.
*/
val absoluteRight = ConstraintLayoutBaseScope.VerticalAnchor(tag, 1)
/**
* The bottom anchor of this layout.
*/
val bottom = ConstraintLayoutBaseScope.HorizontalAnchor(tag, 1)
/**
* The baseline anchor of this layout.
*/
val baseline = ConstraintLayoutBaseScope.BaselineAnchor(tag)
}
/**
* Common scope for [ConstraintLayoutScope] and [ConstraintSetScope], the content being shared
* between the inline DSL API and the ConstraintSet-based API.
*/
abstract class ConstraintLayoutBaseScope {
protected val tasks = mutableListOf<(State) -> Unit>()
fun applyTo(state: State) = tasks.forEach { it(state) }
fun reset() = tasks.clear()
/**
* Represents a vertical anchor (e.g. start/end of a layout, guideline) that layouts
* can link to in their `Modifier.constrainAs` or `constrain` blocks.
*/
data class VerticalAnchor internal constructor(internal val tag: Any, internal val index: Int)
/**
* Represents a horizontal anchor (e.g. top/bottom of a layout, guideline) that layouts
* can link to in their `Modifier.constrainAs` or `constrain` blocks.
*/
data class HorizontalAnchor internal constructor(internal val tag: Any, internal val index: Int)
/**
* Represents a horizontal anchor corresponding to the [FirstBaseline] of a layout that other
* layouts can link to in their `Modifier.constrainAs` or `constrain` blocks.
*/
// TODO(popam): investigate if this can be just a HorizontalAnchor
data class BaselineAnchor internal constructor(internal val tag: Any)
/**
* Creates a guideline at a specific offset from the start of the [ConstraintLayout].
*/
fun createGuidelineFromStart(offset: Dp): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
state.verticalGuideline(tag).apply {
if (state.layoutDirection == LayoutDirection.Ltr) start(offset) else end(offset)
}
}
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a specific offset from the left of the [ConstraintLayout].
*/
fun createGuidelineFromAbsoluteLeft(offset: Dp): VerticalAnchor {
val tag = createTag()
tasks.add { state -> state.verticalGuideline(tag).apply { start(offset) } }
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a specific offset from the start of the [ConstraintLayout].
*/
fun createGuidelineFromStart(percent: Float): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
state.verticalGuideline(tag).apply {
if (state.layoutDirection == LayoutDirection.Ltr) {
percent(percent)
} else {
percent(1f - percent)
}
}
}
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a width percentage from the left of the [ConstraintLayout].
*/
// TODO(popam, b/157781990): this is not really percentage
fun createGuidelineFromAbsoluteLeft(percent: Float): VerticalAnchor {
val tag = createTag()
tasks.add { state -> state.verticalGuideline(tag).apply { percent(percent) } }
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a specific offset from the end of the [ConstraintLayout].
*/
fun createGuidelineFromEnd(offset: Dp): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
state.verticalGuideline(tag).apply {
if (state.layoutDirection == LayoutDirection.Ltr) end(offset) else start(offset)
}
}
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a specific offset from the right of the [ConstraintLayout].
*/
fun createGuidelineFromAbsoluteRight(offset: Dp): VerticalAnchor {
val tag = createTag()
tasks.add { state -> state.verticalGuideline(tag).apply { end(offset) } }
return VerticalAnchor(tag, 0)
}
/**
* Creates a guideline at a width percentage from the end of the [ConstraintLayout].
*/
fun createGuidelineFromEnd(percent: Float): VerticalAnchor {
return createGuidelineFromStart(1f - percent)
}
/**
* Creates a guideline at a width percentage from the right of the [ConstraintLayout].
*/
fun createGuidelineFromAbsoluteRight(percent: Float): VerticalAnchor {
return createGuidelineFromAbsoluteLeft(1f - percent)
}
/**
* Creates a guideline at a specific offset from the top of the [ConstraintLayout].
*/
fun createGuidelineFromTop(offset: Dp): HorizontalAnchor {
val tag = createTag()
tasks.add { state -> state.horizontalGuideline(tag).apply { start(offset) } }
return HorizontalAnchor(tag, 0)
}
/**
* Creates a guideline at a height percentage from the top of the [ConstraintLayout].
*/
fun createGuidelineFromTop(percent: Float): HorizontalAnchor {
val tag = createTag()
tasks.add { state -> state.horizontalGuideline(tag).apply { percent(percent) } }
return HorizontalAnchor(tag, 0)
}
/**
* Creates a guideline at a specific offset from the bottom of the [ConstraintLayout].
*/
fun createGuidelineFromBottom(offset: Dp): HorizontalAnchor {
val tag = createTag()
tasks.add { state -> state.horizontalGuideline(tag).apply { end(offset) } }
return HorizontalAnchor(tag, 0)
}
/**
* Creates a guideline at a height percentage from the bottom of the [ConstraintLayout].
*/
fun createGuidelineFromBottom(percent: Float): HorizontalAnchor {
return createGuidelineFromTop(1f - percent)
}
/**
* Creates and returns a start barrier, containing the specified elements.
*/
fun createStartBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
val direction = if (state.layoutDirection == LayoutDirection.Ltr) {
SolverDirection.LEFT
} else {
SolverDirection.RIGHT
}
state.barrier(tag, direction).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return VerticalAnchor(tag, 0)
}
/**
* Creates and returns a left barrier, containing the specified elements.
*/
fun createAbsoluteLeftBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
state.barrier(tag, SolverDirection.LEFT).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return VerticalAnchor(tag, 0)
}
/**
* Creates and returns a top barrier, containing the specified elements.
*/
fun createTopBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): HorizontalAnchor {
val tag = createTag()
tasks.add { state ->
state.barrier(tag, SolverDirection.TOP).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return HorizontalAnchor(tag, 0)
}
/**
* Creates and returns an end barrier, containing the specified elements.
*/
fun createEndBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
val direction = if (state.layoutDirection == LayoutDirection.Ltr) {
SolverDirection.RIGHT
} else {
SolverDirection.LEFT
}
state.barrier(tag, direction).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return VerticalAnchor(tag, 0)
}
/**
* Creates and returns a right barrier, containing the specified elements.
*/
fun createAbsoluteRightBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val tag = createTag()
tasks.add { state ->
state.barrier(tag, SolverDirection.RIGHT).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return VerticalAnchor(tag, 0)
}
/**
* Creates and returns a bottom barrier, containing the specified elements.
*/
fun createBottomBarrier(
vararg elements: ConstrainedLayoutReference,
margin: Dp = 0.dp
): HorizontalAnchor {
val tag = createTag()
tasks.add { state ->
state.barrier(tag, SolverDirection.BOTTOM).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
return HorizontalAnchor(tag, 0)
}
/**
* Creates a horizontal chain including the referenced layouts.
*/
// TODO(popam, b/157783937): this API should be improved
fun createHorizontalChain(
vararg elements: ConstrainedLayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
) {
tasks.add { state ->
state.horizontalChain(*(elements.map { it.tag }.toTypedArray()))
.also { it.style(chainStyle.style) }
.apply()
if (chainStyle.bias != null) {
state.constraints(elements[0].tag).horizontalBias(chainStyle.bias)
}
}
}
/**
* Creates a vertical chain including the referenced layouts.
*/
// TODO(popam, b/157783937): this API should be improved
fun createVerticalChain(
vararg elements: ConstrainedLayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
) {
tasks.add { state ->
state.verticalChain(*(elements.map { it.tag }.toTypedArray()))
.also { it.style(chainStyle.style) }
.apply()
if (chainStyle.bias != null) {
state.constraints(elements[0].tag).verticalBias(chainStyle.bias)
}
}
}
}
/**
* Scope used by the inline DSL of [ConstraintLayout].
*/
@LayoutScopeMarker
class ConstraintLayoutScope internal constructor() : ConstraintLayoutBaseScope() {
/**
* Creates one [ConstrainedLayoutReference], which needs to be assigned to a layout within the
* [ConstraintLayout] as part of [Modifier.constrainAs]. To create more references at the
* same time, see [createRefs].
*/
fun createRef() = ConstrainedLayoutReference(createTag())
/**
* Convenient way to create multiple [ConstrainedLayoutReference]s, which need to be assigned
* to layouts within the [ConstraintLayout] as part of [Modifier.constrainAs]. To create just
* one reference, see [createRef].
*/
fun createRefs() = ConstrainedLayoutReferences()
/**
* Convenience API for creating multiple [ConstrainedLayoutReference] via [createRefs].
*/
inner class ConstrainedLayoutReferences internal constructor() {
operator fun component1() = createRef()
operator fun component2() = createRef()
operator fun component3() = createRef()
operator fun component4() = createRef()
operator fun component5() = createRef()
operator fun component6() = createRef()
operator fun component7() = createRef()
operator fun component8() = createRef()
operator fun component9() = createRef()
operator fun component10() = createRef()
operator fun component11() = createRef()
operator fun component12() = createRef()
operator fun component13() = createRef()
operator fun component14() = createRef()
operator fun component15() = createRef()
operator fun component16() = createRef()
}
/**
* [Modifier] that defines the constraints, as part of a [ConstraintLayout], of the layout
* element.
*/
fun Modifier.constrainAs(
ref: ConstrainedLayoutReference,
constrainBlock: ConstrainScope.() -> Unit
): Modifier {
// TODO(popam, b/157782492): make equals comparable modifiers here.
return this + object : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?) =
ConstraintLayoutParentData(ref, constrainBlock)
}
}
}
/**
* Scope used by the [ConstraintSet] DSL.
*/
@LayoutScopeMarker
class ConstraintSetScope internal constructor() : ConstraintLayoutBaseScope() {
/**
* Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout]
* [tagged][Modifier.tag] with [tag].
*/
fun createRefFor(tag: Any) = ConstrainedLayoutReference(tag)
/**
* Specifies the constraints associated to the layout tagged with [ref] via [Modifier.tag].
*/
fun constrain(
ref: ConstrainedLayoutReference,
constrainBlock: ConstrainScope.() -> Unit
) = ConstrainScope(ref.tag).apply {
constrainBlock()
this@ConstraintSetScope.tasks.addAll(this.tasks)
}
}
/**
* The style of a horizontal or vertical chain.
*/
class ChainStyle internal constructor(
internal val style: SolverChain,
internal val bias: Float? = null
) {
companion object {
/**
* A chain style that evenly distributes the contained layouts.
*/
val Spread = ChainStyle(SolverChain.SPREAD)
/**
* A chain style where the first and last layouts are affixed to the constraints
* on each end of the chain and the rest are evenly distributed.
*/
val SpreadInside = ChainStyle(SolverChain.SPREAD_INSIDE)
/**
* A chain style where the contained layouts are packed together and placed to the
* center of the available space.
*/
val Packed = Packed(0.5f)
/**
* A chain style where the contained layouts are packed together and placed in
* the available space according to a given [bias].
*/
fun Packed(bias: Float) = ChainStyle(SolverChain.PACKED, bias)
}
}
/**
* Parent data provided by `Modifier.constrainAs`.
*/
private class ConstraintLayoutParentData(
val ref: ConstrainedLayoutReference,
val constrain: ConstrainScope.() -> Unit
)
/**
* Scope used by `Modifier.constrainAs`.
*/
@LayoutScopeMarker
class ConstrainScope internal constructor(internal val tag: Any) {
internal val tasks = mutableListOf<(State) -> Unit>()
internal fun applyTo(state: State) = tasks.forEach { it(state) }
/**
* Reference to the [ConstraintLayout] itself, which can be used to specify constraints
* between itself and its children.
*/
val parent = ConstrainedLayoutReference(SolverState.PARENT)
/**
* The start anchor of the layout - can be constrained using [VerticalAnchorable.linkTo].
*/
val start = VerticalAnchorable(tag, -2)
/**
* The left anchor of the layout - can be constrained using [VerticalAnchorable.linkTo].
*/
val absoluteLeft = VerticalAnchorable(tag, 0)
/**
* The top anchor of the layout - can be constrained using [HorizontalAnchorable.linkTo].
*/
val top = HorizontalAnchorable(tag, 0)
/**
* The end anchor of the layout - can be constrained using [VerticalAnchorable.linkTo].
*/
val end = VerticalAnchorable(tag, -1)
/**
* The right anchor of the layout - can be constrained using [VerticalAnchorable.linkTo].
*/
val absoluteRight = VerticalAnchorable(tag, 1)
/**
* The bottom anchor of the layout - can be constrained using [HorizontalAnchorable.linkTo].
*/
val bottom = HorizontalAnchorable(tag, 1)
/**
* The [FirstBaseline] of the layout - can be constrained using [BaselineAnchorable.linkTo].
*/
val baseline = BaselineAnchorable(tag)
/**
* The width of the [ConstraintLayout] child.
*/
var width: Dimension = Dimension.wrapContent
set(value) {
field = value
tasks.add { state ->
state.constraints(tag).width(
(value as DimensionDescription).toSolverDimension(state)
)
}
}
/**
* The height of the [ConstraintLayout] child.
*/
var height: Dimension = Dimension.wrapContent
set(value) {
field = value
tasks.add { state ->
state.constraints(tag).height(
(value as DimensionDescription).toSolverDimension(state)
)
}
}
/**
* Represents a vertical side of a layout (i.e start and end) that can be anchored using
* [linkTo] in their `Modifier.constrainAs` blocks.
*/
inner class VerticalAnchorable internal constructor(
internal val tag: Any,
internal val index: Int
) {
/**
* Adds a link towards a [ConstraintLayoutBaseScope.VerticalAnchor].
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(anchor: ConstraintLayoutBaseScope.VerticalAnchor, margin: Dp = 0.dp) {
tasks.add { state ->
with(state.constraints(tag)) {
val layoutDirection = state.layoutDirection
val index1 = verticalAnchorIndexToFunctionIndex(index, layoutDirection)
val index2 = verticalAnchorIndexToFunctionIndex(anchor.index, layoutDirection)
verticalAnchorFunctions[index1][index2]
.invoke(this, anchor.tag)
.margin(margin)
}
}
}
}
/**
* Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using
* [linkTo] in their `Modifier.constrainAs` blocks.
*/
inner class HorizontalAnchorable internal constructor(
internal val tag: Any,
internal val index: Int
) {
/**
* Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor].
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(anchor: ConstraintLayoutBaseScope.HorizontalAnchor, margin: Dp = 0.dp) {
tasks.add { state ->
with(state.constraints(tag)) {
horizontalAnchorFunctions[index][anchor.index]
.invoke(this, anchor.tag)
.margin(margin)
}
}
}
}
/**
* Represents the [FirstBaseline] of a layout that can be anchored
* using [linkTo] in their `Modifier.constrainAs` blocks.
*/
inner class BaselineAnchorable internal constructor(internal val tag: Any) {
/**
* Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor].
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(anchor: ConstraintLayoutBaseScope.BaselineAnchor, margin: Dp = 0.dp) {
tasks.add { state ->
with(state.constraints(tag)) {
baselineAnchorFunction.invoke(this, anchor.tag).margin(margin)
}
}
}
}
/**
* Adds both start and end links towards other [ConstraintLayoutBaseScope.HorizontalAnchor]s.
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(
start: ConstraintLayoutBaseScope.VerticalAnchor,
end: ConstraintLayoutBaseScope.VerticalAnchor,
startMargin: Dp = 0.dp,
endMargin: Dp = 0.dp,
@FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
) {
this@ConstrainScope.start.linkTo(start, startMargin)
this@ConstrainScope.end.linkTo(end, endMargin)
tasks.add { state ->
state.constraints(tag).horizontalBias(bias)
}
}
/**
* Adds both top and bottom links towards other [ConstraintLayoutBaseScope.HorizontalAnchor]s.
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(
top: ConstraintLayoutBaseScope.HorizontalAnchor,
bottom: ConstraintLayoutBaseScope.HorizontalAnchor,
topMargin: Dp = 0.dp,
bottomMargin: Dp = 0.dp,
@FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
) {
this@ConstrainScope.top.linkTo(top, topMargin)
this@ConstrainScope.bottom.linkTo(bottom, bottomMargin)
tasks.add { state ->
state.constraints(tag).verticalBias(bias)
}
}
/**
* Adds all start, top, end, bottom links towards
* other [ConstraintLayoutBaseScope.HorizontalAnchor]s.
*/
// TODO(popam, b/158069248): add parameter for gone margin
fun linkTo(
start: ConstraintLayoutBaseScope.VerticalAnchor,
top: ConstraintLayoutBaseScope.HorizontalAnchor,
end: ConstraintLayoutBaseScope.VerticalAnchor,
bottom: ConstraintLayoutBaseScope.HorizontalAnchor,
startMargin: Dp = 0.dp,
topMargin: Dp = 0.dp,
endMargin: Dp = 0.dp,
bottomMargin: Dp = 0.dp,
@FloatRange(from = 0.0, to = 1.0) horizontalBias: Float = 0.5f,
@FloatRange(from = 0.0, to = 1.0) verticalBias: Float = 0.5f
) {
linkTo(start, end, startMargin, endMargin, horizontalBias)
linkTo(top, bottom, topMargin, bottomMargin, verticalBias)
}
/**
* Adds all start, top, end, bottom links towards the corresponding anchors of [other].
* This will center the current layout inside or around (depending on size) [other].
*/
fun centerTo(other: ConstrainedLayoutReference) {
linkTo(other.start, other.top, other.end, other.bottom)
}
/**
* Adds start and end links towards the corresponding anchors of [other].
* This will center horizontally the current layout inside or around (depending on size)
* [other].
*/
fun centerHorizontallyTo(other: ConstrainedLayoutReference) {
linkTo(other.start, other.end)
}
/**
* Adds top and bottom links towards the corresponding anchors of [other].
* This will center vertically the current layout inside or around (depending on size)
* [other].
*/
fun centerVerticallyTo(other: ConstrainedLayoutReference) {
linkTo(other.top, other.bottom)
}
/**
* Adds start and end links towards a vertical [anchor].
* This will center the current layout around the vertical [anchor].
*/
fun centerAround(anchor: ConstraintLayoutBaseScope.VerticalAnchor) {
linkTo(anchor, anchor)
}
/**
* Adds top and bottom links towards a horizontal [anchor].
* This will center the current layout around the horizontal [anchor].
*/
fun centerAround(anchor: ConstraintLayoutBaseScope.HorizontalAnchor) {
linkTo(anchor, anchor)
}
}
/**
* Convenience for creating tags corresponding to layout references that cannot be referred
* to from the outside of the scope (e.g. barriers, layout references in the modifier-based API,
* etc.).
*/
private fun createTag() = object : Any() {}
/**
* Represents a dimension that can be assigned to the width or height of a [ConstraintLayout]
* [child][ConstrainedLayoutReference].
*/
// TODO(popam, b/157781841): It is unfortunate that this interface is top level in ui-layout.
// This will be ok if we move constraint layout to its own module or at least subpackage.
interface Dimension {
/**
* A [Dimension] that can be assigned both min and max bounds.
*/
interface Coercible : Dimension
/**
* A [Dimension] that can be assigned a min bound.
*/
interface MinCoercible : Dimension
/**
* A [Dimension] that can be assigned a max bound.
*/
interface MaxCoercible : Dimension
companion object {
/**
* Creates a [Dimension] representing a suggested dp size. The requested size will
* be respected unless the constraints in the [ConstraintSet] do not allow it. The min
* and max bounds will be respected regardless of the constraints in the [ConstraintSet].
* To make the value fixed (respected regardless the [ConstraintSet]), [value] should
* be used instead.
*/
fun preferredValue(dp: Dp): Dimension.Coercible =
DimensionDescription { state -> SolverDimension.Suggested(state.convertDimension(dp)) }
/**
* Creates a [Dimension] representing a fixed dp size. The size will not change
* according to the constraints in the [ConstraintSet].
*/
fun value(dp: Dp): Dimension =
DimensionDescription { state -> SolverDimension.Fixed(state.convertDimension(dp)) }
/**
* A [Dimension] with suggested wrap content behavior. The wrap content size
* will be respected unless the constraints in the [ConstraintSet] do not allow it.
* To make the value fixed (respected regardless the [ConstraintSet]), [wrapContent]
* should be used instead.
*/
val preferredWrapContent: Dimension.Coercible
get() = DimensionDescription { SolverDimension.Suggested(WRAP_DIMENSION) }
/**
* A [Dimension] with fixed wrap content behavior. The size will not change
* according to the constraints in the [ConstraintSet].
*/
val wrapContent: Dimension
get() = DimensionDescription { SolverDimension.Fixed(WRAP_DIMENSION) }
/**
* A [Dimension] that spreads to match constraints. Links should be specified from both
* sides corresponding to this dimension, in order for this to work.
*/
val fillToConstraints: Dimension
get() = DimensionDescription { SolverDimension.Suggested(SPREAD_DIMENSION) }
/**
* A [Dimension] that is a percent of the parent in the corresponding direction.
*/
fun percent(percent: Float): Dimension =
// TODO(popam, b/157880732): make this nicer when possible in future solver releases
DimensionDescription { SolverDimension.Percent(0, percent).suggested(0) }
}
}
/**
* Sets the lower bound of the current [Dimension] to be the wrap content size of the child.
*/
val Dimension.Coercible.atLeastWrapContent: Dimension.MaxCoercible
get() = (this as DimensionDescription).also { it.minSymbol = WRAP_DIMENSION }
/**
* Sets the lower bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.Coercible.atLeast(dp: Dp): Dimension.MaxCoercible =
(this as DimensionDescription).also { it.min = dp }
/**
* Sets the upper bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.Coercible.atMost(dp: Dp): Dimension.MinCoercible =
(this as DimensionDescription).also { it.max = dp }
/**
* Sets the upper bound of the current [Dimension] to be the wrap content size of the child.
*/
val Dimension.Coercible.atMostWrapContent: Dimension.MinCoercible
get() = (this as DimensionDescription).also { it.maxSymbol = WRAP_DIMENSION }
/**
* Sets the lower bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.MinCoercible.atLeastWrapContent(dp: Dp): Dimension =
(this as DimensionDescription).also { it.min = dp }
/**
* Sets the lower bound of the current [Dimension] to be the wrap content size of the child.
*/
val Dimension.MinCoercible.atLeastWrapContent: Dimension
get() = (this as DimensionDescription).also { it.minSymbol = WRAP_DIMENSION }
/**
* Sets the upper bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.MaxCoercible.atMost(dp: Dp): Dimension =
(this as DimensionDescription).also { it.max = dp }
/**
* Sets the upper bound of the current [Dimension] to be the [Wrap] size of the child.
*/
val Dimension.MaxCoercible.atMostWrapContent: Dimension
get() = (this as DimensionDescription).also { it.maxSymbol = WRAP_DIMENSION }
/**
* Describes a sizing behavior that can be applied to the width or height of a
* [ConstraintLayout] child. The content of this class should not be instantiated
* directly; helpers available in the [Dimension]'s companion object should be used.
*/
internal class DimensionDescription internal constructor(
private val baseDimension: (State) -> SolverDimension
) : Dimension.Coercible, Dimension.MinCoercible, Dimension.MaxCoercible, Dimension {
var min: Dp? = null
var minSymbol: Any? = null
var max: Dp? = null
var maxSymbol: Any? = null
internal fun toSolverDimension(state: State) = baseDimension(state).also {
if (minSymbol != null) {
it.min(minSymbol)
} else if (min != null) {
it.min(state.convertDimension(min!!))
}
if (maxSymbol != null) {
it.max(maxSymbol)
} else if (max != null) {
it.max(state.convertDimension(max!!))
}
}
}
/**
* Immutable description of the constraints used to layout the children of a [ConstraintLayout].
*/
@Immutable
interface ConstraintSet {
/**
* Applies the [ConstraintSet] to a state.
*/
fun applyTo(state: State, measurables: List<Measurable>)
}
@Deprecated("This API for building ConstraintSets has been deprecated. " +
"Consider using the inline DSL API - see ConstraintLayoutDemo for an example," +
"or use ConstraintSet2 for building constraint sets.")
fun ConstraintSet(description: ConstraintSetBuilderScope.() -> Unit) = object : ConstraintSet {
override fun applyTo(state: State, measurables: List<Measurable>) {
measurables.forEach { measurable ->
state.map((measurable.tag ?: createTag()), measurable)
}
description(ConstraintSetBuilderScope(state))
}
}
/**
* Creates a [ConstraintSet].
*/
fun ConstraintSet2(description: ConstraintSetScope.() -> Unit) = object : ConstraintSet {
override fun applyTo(state: State, measurables: List<Measurable>) {
measurables.forEach { measurable ->
state.map((measurable.tag ?: createTag()), measurable)
}
val scope = ConstraintSetScope()
scope.description()
scope.applyTo(state)
}
}
/**
* Builder scope for a [ConstraintSet]. The scope should not be created directly - the
* [ConstraintSet] class constructor should be used instead.
*/
// TODO(popam): support RTL
@Deprecated("This way of building ConstraintSets has been deprecated. " +
"Use ConstraintSet2 (and its implicit ConstraintSetScope) instead.")
class ConstraintSetBuilderScope internal constructor(internal val state: State) {
/**
* Creates a reference corresponding to the constraint layout children with a specific tag,
* which can be used to define the constraints to be imposed to those children.
*/
fun tag(tag: Any) = tags.getOrPut(tag, { ConstrainedLayoutReference(tag) })
private val tags = mutableMapOf<Any, ConstrainedLayoutReference>()
/**
* Reference to the [ConstraintLayout] itself, which can be used to specify constraints
* between itself and its children.
*/
val parent = ConstrainedLayoutReference(SolverState.PARENT)
/**
* Represents a dimension that can be assigned to the width or height of a [ConstraintLayout]
* [child][ConstrainedLayoutReference].
*/
interface Dimension {
/**
* A [Dimension] that can be assigned both min and max bounds.
*/
interface Coercible : Dimension
/**
* A [Dimension] that can be assigned a min bound.
*/
interface MinCoercible : Dimension
/**
* A [Dimension] that can be assigned a max bound.
*/
interface MaxCoercible : Dimension
}
/**
* Sets the lower bound of the current [Dimension] to be the [Wrap] size of the child.
*/
val Dimension.Coercible.minWrap: Dimension.MaxCoercible
get() = (this as DimensionDescription).also { it.minSymbol = WRAP_DIMENSION }
/**
* Sets the lower bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.Coercible.min(dp: Dp): Dimension.MaxCoercible =
(this as DimensionDescription).also { it.min = state.convertDimension(dp).ipx }
/**
* Sets the upper bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.Coercible.max(dp: Dp): Dimension.MinCoercible =
(this as DimensionDescription).also { it.max = state.convertDimension(dp).ipx }
/**
* Sets the upper bound of the current [Dimension] to be the [Wrap] size of the child.
*/
val Dimension.Coercible.maxWrap: Dimension.MinCoercible
get() = (this as DimensionDescription).also { it.maxSymbol = WRAP_DIMENSION }
/**
* Sets the lower bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.MinCoercible.min(dp: Dp): Dimension =
(this as DimensionDescription).also { it.min = state.convertDimension(dp).ipx }
/**
* Sets the lower bound of the current [Dimension] to be the [Wrap] size of the child.
*/
val Dimension.MinCoercible.minWrap: Dimension
get() = (this as DimensionDescription).also { it.minSymbol = WRAP_DIMENSION }
/**
* Sets the upper bound of the current [Dimension] to a fixed [dp] value.
*/
fun Dimension.MaxCoercible.max(dp: Dp): Dimension =
(this as DimensionDescription).also { it.max = state.convertDimension(dp).ipx }
/**
* Sets the upper bound of the current [Dimension] to be the [Wrap] size of the child.
*/
val Dimension.MaxCoercible.maxWrap: Dimension
get() = (this as DimensionDescription).also { it.maxSymbol = WRAP_DIMENSION }
/**
* Describes a sizing behavior that can be applied to the width or height of a
* [ConstraintLayout] child. The content of this class should not be instantiated
* directly; helpers available in the scope such as [wrap], [value]
* or [wrapFixed] should be used instead to create a [Dimension].
*/
internal class DimensionDescription internal constructor(
private val baseDimension: SolverDimension
) : Dimension.Coercible, Dimension.MinCoercible, Dimension.MaxCoercible, Dimension {
var min: IntPx? = null
var minSymbol: Any? = null
var max: IntPx? = null
var maxSymbol: Any? = null
internal fun toSolverDimension() = baseDimension.also {
if (minSymbol != null) {
it.min(minSymbol)
} else if (min != null) {
it.min(min!!.value)
}
if (maxSymbol != null) {
it.max(maxSymbol)
} else if (max != null) {
it.max(max!!.value)
}
}
}
/**
* Creates a [Dimension] representing a suggested dp size. The requested size will
* be respected unless the constraints in the [ConstraintSet] do not allow it. The min
* and max bounds will be respected regardless of the constraints in the [ConstraintSet].
* To make the value fixed (respected regardless the [ConstraintSet]), [valueFixed] should
* be used instead.
*/
fun value(dp: Dp): Dimension.Coercible =
DimensionDescription(SolverDimension.Suggested(state.convertDimension(dp)))
/**
* Creates a [Dimension] representing a fixed dp size. The size will not change
* according to the constraints in the [ConstraintSet].
*/
fun valueFixed(dp: Dp): Dimension =
DimensionDescription(SolverDimension.Fixed(state.convertDimension(dp)))
/**
* A [Dimension] with suggested wrap content behavior. The wrap content size
* will be respected unless the constraints in the [ConstraintSet] do not allow it.
* To make the value fixed (respected regardless the [ConstraintSet]), [wrapFixed]
* should be used instead.
*/
val wrap: Dimension.Coercible
get() = DimensionDescription(SolverDimension.Suggested(WRAP_DIMENSION))
/**
* A [Dimension] with fixed wrap content behavior. The size will not change
* according to the constraints in the [ConstraintSet].
*/
val wrapFixed: Dimension
get() = DimensionDescription(SolverDimension.Fixed(WRAP_DIMENSION))
/**
* A [Dimension] that spreads to match constraints.
*/
val spread: Dimension
get() = DimensionDescription(SolverDimension.Suggested(SPREAD_DIMENSION))
/**
* A [Dimension] that is a percent of the parent in the corresponding direction.
*/
fun percent(percent: Float): Dimension =
// TODO(popam): make this nicer when possible in future solver releases
DimensionDescription(SolverDimension.Percent(0, percent).suggested(0))
inner class ConstrainedLayoutReference internal constructor(val tag: Any) {
val left = VerticalAnchor.ConstrainedLayoutAnchor(state, this, 0)
val top = HorizontalAnchor.ConstrainedLayoutAnchor(state, this, 0)
var right = VerticalAnchor.ConstrainedLayoutAnchor(state, this, 1)
var bottom = HorizontalAnchor.ConstrainedLayoutAnchor(state, this, 1)
var baseline = ConstrainedLayoutBaselineAnchor(state, this)
/**
* The width of the [ConstraintLayout] child.
*/
var width: Dimension = wrap
set(value) {
field = value
state.constraints(tag).width(
(value as DimensionDescription).toSolverDimension()
).apply()
}
/**
* The height of the [ConstraintLayout] child.
*/
var height: Dimension = wrap
set(value) {
field = value
state.constraints(tag).height(
(value as DimensionDescription).toSolverDimension()
).apply()
}
/**
* Adds constraints between left, top, right and bottom corresponding anchors of
* two layout references.
*/
infix fun constrainTo(other: ConstrainedLayoutReference) {
left constrainTo other.left
top constrainTo other.top
right constrainTo other.right
bottom constrainTo other.bottom
}
/**
* Adds constraints between left and right corresponding anchors of two layout references.
*/
infix fun constrainHorizontallyTo(other: ConstrainedLayoutReference) {
left constrainTo other.left
right constrainTo other.right
}
/**
* Adds constraints between top and bottom corresponding anchors of two layout references.
*/
infix fun constrainVerticallyTo(other: ConstrainedLayoutReference) {
top constrainTo other.top
bottom constrainTo other.bottom
}
/**
* The horizontal bias of the current layout reference.
*/
// TODO(popam): keep the source of truth in ConstraintReference or make this write only
var horizontalBias: Float = 0.5f
set(value) {
field = value
state.constraints(tag).horizontalBias(value)
}
/**
* The vertical bias of the current layout reference.
*/
// TODO(popam): keep the source of truth in ConstraintReference or make this write only
var verticalBias: Float = 0.5f
set(value) {
field = value
state.constraints(tag).verticalBias(value)
}
/**
* Centers the layout horizontally in its [parent].
*/
fun centerHorizontally() = state.centerHorizontally(tag).also { it.bias(0.5f) }.apply()
/**
* Centers the layout vertically in its [parent].
*/
fun centerVertically() = state.centerVertically(tag).also { it.bias(0.5f) }.apply()
/**
* Centers the layout in its [parent].
*/
fun center() {
centerHorizontally()
centerVertically()
}
}
/**
* Represents a horizontal chain.
*/
class HorizontalChain internal constructor(
internal val first: ConstrainedLayoutReference,
internal val last: ConstrainedLayoutReference
) {
val left: VerticalAnchor.ConstrainedLayoutAnchor get() = first.left
val right: VerticalAnchor.ConstrainedLayoutAnchor get() = last.right
infix fun constrainTo(other: ConstrainedLayoutReference) {
left constrainTo other.left
right constrainTo other.right
}
}
/**
* Represents a vertical chain.
*/
class VerticalChain internal constructor(
internal val first: ConstrainedLayoutReference,
internal val last: ConstrainedLayoutReference
) {
val top: HorizontalAnchor.ConstrainedLayoutAnchor get() = first.top
val bottom: HorizontalAnchor.ConstrainedLayoutAnchor get() = last.bottom
infix fun constrainTo(other: ConstrainedLayoutReference) {
first.top constrainTo other.top
last.bottom constrainTo other.bottom
}
}
/**
* Defines a vertical anchor which can be used for defining constraints. It can correspond:
* - to a side or baseline of a child of the [ConstraintLayout]
* - to a left barrier or right barrier
* - to a vertical guideline
*/
sealed class VerticalAnchor {
internal abstract val state: State
internal abstract val tag: Any
internal abstract val index: Int
/**
* Anchor corresponding to the left or right of a child of the [ConstraintLayout].
*/
class ConstrainedLayoutAnchor internal constructor(
override val state: State,
internal val constrainedLayoutReference: ConstrainedLayoutReference,
override val index: Int
) : VerticalAnchor() {
override val tag: Any get() = constrainedLayoutReference.tag
// TODO(popam): keep the source of truth in ConstraintReference
/**
* The margin to be applied to the current [ConstrainedLayoutAnchor].
*/
var margin: Dp = 0.dp
set(value) {
field = value
state.constraints(tag)
.let { if (index == 0) it.start() else it.end() }
.margin(value)
}
/**
* Adds a constraint between a [ConstrainedLayoutAnchor] and a [VerticalAnchor].
*/
infix fun constrainTo(other: VerticalAnchor) {
with(state.constraints(this.constrainedLayoutReference.tag)) {
val thisIndex = this@ConstrainedLayoutAnchor.index
val otherIndex = other.index
verticalAnchorFunctions[thisIndex][otherIndex].invoke(this, other.tag)
}
}
}
/**
* Anchor corresponding to a vertical guideline.
*/
class GuidelineAnchor internal constructor(
override val state: State,
override val tag: Any,
override val index: Int = 0
) : VerticalAnchor()
/**
* Anchor corresponding to a left or right barrier.
*/
class BarrierAnchor internal constructor(
override val state: State,
override val tag: Any,
private val barrierReference: BarrierReference,
override val index: Int = 0
) : VerticalAnchor() {
// TODO(popam): keep the source of truth in ConstraintReference
/**
* The margin to be applied to the current [BarrierAnchor], in the
* direction of the barrier.
*/
var margin: Dp = 0.dp
set(value) {
field = value
barrierReference.margin(value)
}
}
}
/**
* Defines an horizontal anchor which can be used for defining constraints. It can correspond:
* - to a side or baseline of a child of the [ConstraintLayout]
* - to a top or bottom barrier
* - to a horizontal guideline
*/
sealed class HorizontalAnchor {
internal abstract val state: State
internal abstract val tag: Any
internal abstract val index: Int
/**
* Anchor corresponding to the top or bottom of a child of the [ConstraintLayout].
*/
class ConstrainedLayoutAnchor internal constructor(
override val state: State,
internal val constrainedLayoutReference: ConstrainedLayoutReference,
override val index: Int
) : HorizontalAnchor() {
override val tag: Any get() = constrainedLayoutReference.tag
// TODO(popam): keep the source of truth in ConstraintReference
/**
* The margin to be applied to the current [ConstrainedLayoutAnchor].
*/
var margin: Dp = 0.dp
set(value) {
field = value
state.constraints(tag)
.let { if (index == 0) it.top() else it.bottom() }
.margin(value)
}
/**
* Adds a constraint between a [ConstrainedLayoutAnchor] and a [HorizontalAnchor].
*/
infix fun constrainTo(other: HorizontalAnchor) {
with(state.constraints(this.constrainedLayoutReference.tag)) {
val thisIndex = this@ConstrainedLayoutAnchor.index
val otherIndex = other.index
horizontalAnchorFunctions[thisIndex][otherIndex].invoke(this, other.tag)
}
}
}
/**
* Anchor corresponding to a horizontal guideline.
*/
class GuidelineAnchor internal constructor(
override val state: State,
override val tag: Any,
override val index: Int = 0
) : HorizontalAnchor()
/**
* Anchor corresponding to a top or bottom barrier.
*/
class BarrierAnchor internal constructor(
override val state: State,
override val tag: Any,
private val barrierReference: BarrierReference,
override val index: Int = 0
) : HorizontalAnchor() {
// TODO(popam): keep the source of truth in ConstraintReference
var margin: Dp = 0.dp
set(value) {
field = value
barrierReference.margin(value)
}
}
}
/**
* Anchor corresponding to the baseline of a [ConstraintLayout] child.
*/
class ConstrainedLayoutBaselineAnchor internal constructor(
val state: State,
val tag: Any
) {
/**
* Adds a constraint between two [ConstrainedLayoutBaselineAnchor] anchors.
*/
infix fun constrainTo(other: ConstrainedLayoutBaselineAnchor) {
with(state.constraints(tag)) {
baselineAnchorFunction.invoke(this, other.tag)
}
}
}
/**
* Creates a horizontal chain including the referenced layouts.
*/
@SuppressLint
fun createHorizontalChain(
// Suppress lint here to allow vararg parameter for elements. API likely to change.
@SuppressLint("ArrayReturn")
vararg elements: ConstrainedLayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
): HorizontalChain {
state.horizontalChain(*(elements.map { it.tag }.toTypedArray()))
.also { it.style(chainStyle.style) }
.apply()
if (chainStyle.bias != null) elements[0].horizontalBias = chainStyle.bias
return HorizontalChain(elements.first(), elements.last())
}
/**
* Creates a vertical chain including the referenced layouts.
*/
fun createVerticalChain(
// Suppress lint here to allow vararg parameter for elements. API likely to change.
@SuppressLint("ArrayReturn")
vararg elements: ConstrainedLayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
): VerticalChain {
state.verticalChain(*(elements.map { it.tag }.toTypedArray()))
.also { it.style(chainStyle.style) }
.apply()
if (chainStyle.bias != null) elements[0].verticalBias = chainStyle.bias
return VerticalChain(elements.first(), elements.last())
}
/**
* The style of a horizontal or vertical chain.
*/
class ChainStyle internal constructor(
internal val style: SolverChain,
internal val bias: Float? = null
) {
companion object {
/**
* A chain style that evenly distributes the contained layouts.
*/
val Spread = ChainStyle(SolverChain.SPREAD)
/**
* A chain style where the first and last layouts are affixed to the constraints
* on each end of the chain and the rest are evenly distributed.
*/
val SpreadInside = ChainStyle(SolverChain.SPREAD_INSIDE)
/**
* A chain style where the contained layouts are packed together and placed to the
* center of the available space.
*/
val Packed = Packed(0.5f)
/**
* A chain style where the contained layouts are packed together and placed in
* the available space according to a given [bias].
*/
fun Packed(bias: Float) = ChainStyle(SolverChain.PACKED, bias)
}
}
/**
* Creates a guideline at a specific offset from the left of the [ConstraintLayout].
*/
fun createGuidelineFromLeft(offset: Dp): VerticalAnchor.GuidelineAnchor {
val tag = createTag()
state.verticalGuideline(tag).apply { start(offset) }
return VerticalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a width percentage from the left of the [ConstraintLayout].
*/
fun createGuidelineFromLeft(percent: Float): VerticalAnchor.GuidelineAnchor {
val tag = createTag()
state.verticalGuideline(tag).apply { percent(percent) }
return VerticalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a specific offset from the right of the [ConstraintLayout].
*/
fun createGuidelineFromRight(offset: Dp): VerticalAnchor.GuidelineAnchor {
val tag = createTag()
state.verticalGuideline(tag).apply { end(offset) }
return VerticalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a width percentage from the right of the [ConstraintLayout].
*/
fun createGuidelineFromRight(percent: Float): VerticalAnchor.GuidelineAnchor {
return createGuidelineFromLeft(1f - percent)
}
/**
* Creates a guideline at a specific offset from the top of the [ConstraintLayout].
*/
fun createGuidelineFromTop(offset: Dp): HorizontalAnchor.GuidelineAnchor {
val tag = createTag()
state.horizontalGuideline(tag).apply { start(offset) }
return HorizontalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a height percentage from the top of the [ConstraintLayout].
*/
fun createGuidelineFromTop(percent: Float): HorizontalAnchor.GuidelineAnchor {
val tag = createTag()
state.horizontalGuideline(tag).apply { percent(percent) }
return HorizontalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a specific offset from the bottom of the [ConstraintLayout].
*/
fun createGuidelineFromBottom(offset: Dp): HorizontalAnchor.GuidelineAnchor {
val tag = createTag()
state.horizontalGuideline(tag).apply { end(offset) }
return HorizontalAnchor.GuidelineAnchor(state, tag)
}
/**
* Creates a guideline at a height percentage from the bottom of the [ConstraintLayout].
*/
fun createGuidelineFromBottom(percent: Float): HorizontalAnchor.GuidelineAnchor {
return createGuidelineFromTop(1f - percent)
}
/**
* Creates and returns a top barrier, containing the specified elements.
*/
fun createTopBarrier(
vararg elements: ConstrainedLayoutReference
): HorizontalAnchor.BarrierAnchor {
val tag = createTag()
val barrier = state.barrier(tag, SolverDirection.TOP).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}
return HorizontalAnchor.BarrierAnchor(state, tag, barrier)
}
/**
* Creates and returns a bottom barrier, containing the specified elements.
*/
fun createBottomBarrier(
vararg elements: ConstrainedLayoutReference
): HorizontalAnchor.BarrierAnchor {
val tag = createTag()
val barrier = state.barrier(tag, SolverDirection.BOTTOM).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}
return HorizontalAnchor.BarrierAnchor(state, tag, barrier)
}
/**
* Creates and returns a left barrier, containing the specified elements.
*/
fun createLeftBarrier(
vararg elements: ConstrainedLayoutReference
): VerticalAnchor.BarrierAnchor {
val tag = createTag()
val barrier = state.barrier(tag, SolverDirection.START).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}
return VerticalAnchor.BarrierAnchor(state, tag, barrier)
}
/**
* Creates and returns a right barrier, containing the specified elements.
*/
fun createRightBarrier(
vararg elements: ConstrainedLayoutReference
): VerticalAnchor.BarrierAnchor {
val tag = createTag()
val barrier = state.barrier(tag, SolverDirection.END).apply {
add(*(elements.map { it.tag }.toTypedArray()))
}
return VerticalAnchor.BarrierAnchor(state, tag, barrier)
}
internal companion object {
val verticalAnchorFunctions:
Array<Array<ConstraintReference.(Any) -> ConstraintReference>> = arrayOf(
arrayOf(
{ other -> leftToLeft(other) },
{ other -> leftToRight(other) }
),
arrayOf(
{ other -> rightToLeft(other) },
{ other -> rightToRight(other) }
)
)
/**
* Converts the index (-2 -> start, -1 -> end, 0 -> left, 1 -> right) to an index in
* the arrays above (0 -> left, 1 -> right).
*/
// TODO(popam, b/157886946): this is temporary until we can use CL's own RTL handling
fun verticalAnchorIndexToFunctionIndex(index: Int, layoutDirection: LayoutDirection) =
when {
index >= 0 -> index // already left or right
layoutDirection == LayoutDirection.Ltr -> 2 + index // start -> left, end -> right
else -> -index - 1 // start -> right, end -> left
}
val horizontalAnchorFunctions:
Array<Array<ConstraintReference.(Any) -> ConstraintReference>> = arrayOf(
arrayOf(
{ other -> topToTop(other) },
{ other -> topToBottom(other) }
),
arrayOf(
{ other -> bottomToTop(other) },
{ other -> bottomToBottom(other) }
)
)
val baselineAnchorFunction: ConstraintReference.(Any) -> ConstraintReference =
{ other -> baselineToBaseline(other) }
}
}
/**
* The state of the [ConstraintLayout] solver.
*/
class State(val density: Density) : SolverState() {
lateinit var rootIncomingConstraints: Constraints
lateinit var layoutDirection: LayoutDirection
override fun convertDimension(value: Any?): Int {
return if (value is Dp) {
with(density) { value.toIntPx().value }
} else {
super.convertDimension(value)
}
}
}
private class Measurer internal constructor() : BasicMeasure.Measurer {
private val root = ConstraintWidgetContainer(0, 0).also { it.measurer = this }
private val placeables = mutableMapOf<Measurable, Placeable>()
private lateinit var density: Density
private lateinit var measureScope: MeasureScope
private val state by lazy(LazyThreadSafetyMode.NONE) { State(density) }
val widthConstraintsHolder = IntArray(2)
val heightConstraintsHolder = IntArray(2)
override fun measure(constraintWidget: ConstraintWidget, measure: BasicMeasure.Measure) {
val measurable = constraintWidget.companionWidget
if (measurable !is Measurable) return
if (DEBUG) {
Log.d(
"CCL", "Measuring ${measurable.tag} with: " +
constraintWidget.toDebugString() + "\n" + measure.toDebugString()
)
}
val initialPlaceable = placeables[measurable]
val initialWidth = initialPlaceable?.width?.value ?: constraintWidget.width
val initialHeight = initialPlaceable?.height?.value ?: constraintWidget.height
val initialBaseline =
initialPlaceable?.get(FirstBaseline) ?: constraintWidget.baselineDistance
var constraints = run {
obtainConstraints(
constraintWidget.horizontalDimensionBehaviour,
constraintWidget.width,
constraintWidget.mMatchConstraintDefaultWidth,
measure.useDeprecated,
state.rootIncomingConstraints.maxWidth.value,
widthConstraintsHolder
)
obtainConstraints(
constraintWidget.verticalDimensionBehaviour,
constraintWidget.height,
constraintWidget.mMatchConstraintDefaultHeight,
measure.useDeprecated,
state.rootIncomingConstraints.maxHeight.value,
heightConstraintsHolder
)
Constraints(
widthConstraintsHolder[0].ipx,
widthConstraintsHolder[1].ipx,
heightConstraintsHolder[0].ipx,
heightConstraintsHolder[1].ipx
)
}
if (constraintWidget.horizontalDimensionBehaviour != MATCH_CONSTRAINT ||
constraintWidget.mMatchConstraintDefaultWidth != MATCH_CONSTRAINT_SPREAD ||
constraintWidget.verticalDimensionBehaviour != MATCH_CONSTRAINT ||
constraintWidget.mMatchConstraintDefaultHeight != MATCH_CONSTRAINT_SPREAD
) {
if (DEBUG) {
Log.d("CCL", "Measuring ${measurable.tag} with $constraints")
}
val placeable = with(measureScope) {
measurable.measure(constraints).also { placeables[measurable] = it }
}
if (DEBUG) {
Log.d("CCL", "${measurable.tag} is size ${placeable.width} ${placeable.height}")
}
val coercedWidth = placeable.width.value.coerceIn(
constraintWidget.minWidth.takeIf { it > 0 },
constraintWidget.maxWidth.takeIf { it > 0 }
)
val coercedHeight = placeable.height.value.coerceIn(
constraintWidget.minHeight.takeIf { it > 0 },
constraintWidget.maxHeight.takeIf { it > 0 }
)
var remeasure = false
if (coercedWidth != placeable.width.value) {
constraints = constraints.copy(
minWidth = coercedWidth.ipx,
maxWidth = coercedWidth.ipx
)
remeasure = true
}
if (coercedHeight != placeable.height.value) {
constraints = constraints.copy(
minHeight = coercedHeight.ipx,
maxHeight = coercedHeight.ipx
)
remeasure = true
}
if (remeasure) {
if (DEBUG) {
Log.d("CCL", "Remeasuring coerced ${measurable.tag} with $constraints")
}
with(measureScope) {
measurable.measure(constraints).also { placeables[measurable] = it }
}
}
}
val currentPlaceable = placeables[measurable]
measure.measuredWidth = currentPlaceable?.width?.value ?: constraintWidget.width
measure.measuredHeight = currentPlaceable?.height?.value ?: constraintWidget.height
val baseline = currentPlaceable?.get(FirstBaseline)
measure.measuredHasBaseline = baseline != null
if (baseline != null) measure.measuredBaseline = baseline.value
measure.measuredNeedsSolverPass = measure.measuredWidth != initialWidth ||
measure.measuredHeight != initialHeight ||
measure.measuredBaseline != initialBaseline
}
/**
* Calculates the [Constraints] in one direction that should be used to measure a child,
* based on the solver measure request.
*/
private fun obtainConstraints(
dimensionBehaviour: ConstraintWidget.DimensionBehaviour,
dimension: Int,
matchConstraintDefaultDimension: Int,
useDeprecated: Boolean,
rootMaxConstraint: Int,
outConstraints: IntArray
) = when (dimensionBehaviour) {
FIXED -> {
outConstraints[0] = dimension
outConstraints[1] = dimension
}
WRAP_CONTENT -> {
outConstraints[0] = 0
outConstraints[1] = rootMaxConstraint
}
MATCH_CONSTRAINT -> {
val useDimension =
useDeprecated && matchConstraintDefaultDimension == MATCH_CONSTRAINT_WRAP
outConstraints[0] = if (useDimension) dimension else 0
outConstraints[1] = if (useDimension) dimension else rootMaxConstraint
}
else -> {
error("MATCH_PARENT is not supported")
}
}
fun performMeasure(
constraints: Constraints,
layoutDirection: LayoutDirection,
constraintSet: ConstraintSet,
measurables: List<Measurable>,
measureScope: MeasureScope
): IntPxSize {
this.density = measureScope
this.measureScope = measureScope
state.reset()
// Define the size of the ConstraintLayout.
state.width(
if (constraints.hasFixedWidth) {
SolverDimension.Fixed(constraints.maxWidth.value)
} else {
SolverDimension.Wrap().min(constraints.minWidth.value)
}
)
state.height(
if (constraints.hasFixedHeight) {
SolverDimension.Fixed(constraints.maxHeight.value)
} else {
SolverDimension.Wrap().min(constraints.minHeight.value)
}
)
// Build constraint set and apply it to the state.
state.rootIncomingConstraints = constraints
state.layoutDirection = layoutDirection
constraintSet.applyTo(state, measurables)
state.apply(root)
root.width = constraints.maxWidth.value
root.height = constraints.maxHeight.value
root.updateHierarchy()
if (DEBUG) {
root.debugName = "ConstraintLayout"
root.children.forEach { child ->
child.debugName = (child.companionWidget as? Measurable)?.tag?.toString() ?: "NOTAG"
}
Log.d("CCL", "ConstraintLayout is asked to measure with $constraints")
Log.d("CCL", root.toDebugString())
for (child in root.children) {
Log.d("CCL", child.toDebugString())
}
}
// No need to set sizes and size modes as we passed them to the state above.
root.measure(Optimizer.OPTIMIZATION_NONE, 0, 0, 0, 0, 0, 0, 0, 0)
for (child in root.children) {
val measurable = child.companionWidget
if (measurable !is Measurable) continue
val placeable = placeables[measurable]
val currentWidth = placeable?.width?.value
val currentHeight = placeable?.height?.value
if (child.width != currentWidth || child.height != currentHeight) {
if (DEBUG) {
Log.d(
"CCL",
"Final measurement for ${measurable.tag} " +
"to confirm size ${child.width} ${child.height}"
)
}
with(measureScope) {
measurable.measure(
Constraints.fixed(child.width.ipx, child.height.ipx)
).also { placeables[measurable] = it }
}
}
}
if (DEBUG) {
Log.d("CCL", "ConstraintLayout is at the end ${root.width} ${root.height}")
}
return IntPxSize(root.width.ipx, root.height.ipx)
}
fun Placeable.PlacementScope.performLayout() {
for (child in root.children) {
val measurable = child.companionWidget
if (measurable !is Measurable) continue
// TODO(popam, b/157886946): check if measurer's rtl support should be used instead
placeables[measurable]?.placeAbsolute(child.x.ipx, child.y.ipx)
}
}
override fun didMeasures() { }
}
private typealias SolverDimension = androidx.constraintlayout.core.state.Dimension
private typealias SolverState = androidx.constraintlayout.core.state.State
private typealias SolverDirection = androidx.constraintlayout.core.state.State.Direction
private typealias SolverChain = androidx.constraintlayout.core.state.State.Chain
private val DEBUG = true
private fun ConstraintWidget.toDebugString() =
"$debugName " +
"width $width minWidth $minWidth maxWidth $maxWidth " +
"height $height minHeight $minHeight maxHeight maxHeight " +
"HDB $horizontalDimensionBehaviour VDB $verticalDimensionBehaviour " +
"percentH $mMatchConstraintPercentWidth $mMatchConstraintPercentHeight"
private fun BasicMeasure.Measure.toDebugString() =
"use deprecated is $useDeprecated "