/*
* Copyright (C) 2021 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.constraintlayout.compose
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.core.widgets.ConstraintWidget
/**
* 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): Unit = tasks.forEach { it(state) }
open fun reset() {
tasks.clear()
helperId = HelpersStartId
helpersHashCode = 0
}
@PublishedApi
internal var helpersHashCode: Int = 0
private fun updateHelpersHashCode(value: Int) {
helpersHashCode = (helpersHashCode * 1009 + value) % 1000000007
}
private val HelpersStartId = 1000
private var helperId = HelpersStartId
private fun createHelperId() = helperId++
/**
* Represents a vertical anchor (e.g. start/end of a layout, guideline) that layouts
* can link to in their `Modifier.constrainAs` or `constrain` blocks.
*
* @param reference The [LayoutReference] that this anchor belongs to.
*/
@Stable
data class VerticalAnchor internal constructor(
internal val id: Any,
internal val index: Int,
val reference: LayoutReference
)
/**
* Represents a horizontal anchor (e.g. top/bottom of a layout, guideline) that layouts
* can link to in their `Modifier.constrainAs` or `constrain` blocks.
*
* @param reference The [LayoutReference] that this anchor belongs to.
*/
@Stable
data class HorizontalAnchor internal constructor(
internal val id: Any,
internal val index: Int,
val reference: LayoutReference
)
/**
* Represents a horizontal anchor corresponding to the [FirstBaseline] of a layout that other
* layouts can link to in their `Modifier.constrainAs` or `constrain` blocks.
*
* @param reference The [LayoutReference] that this anchor belongs to.
*/
// TODO(popam): investigate if this can be just a HorizontalAnchor
@Stable
data class BaselineAnchor internal constructor(
internal val id: Any,
val reference: LayoutReference
)
/**
* Specifies additional constraints associated to the horizontal chain identified with [ref].
*/
fun constrain(
ref: HorizontalChainReference,
constrainBlock: HorizontalChainScope.() -> Unit
): HorizontalChainScope = HorizontalChainScope(ref.id).apply {
constrainBlock()
this@ConstraintLayoutBaseScope.tasks.addAll(this.tasks)
}
/**
* Specifies additional constraints associated to the vertical chain identified with [ref].
*/
fun constrain(
ref: VerticalChainReference,
constrainBlock: VerticalChainScope.() -> Unit
): VerticalChainScope = VerticalChainScope(ref.id).apply {
constrainBlock()
this@ConstraintLayoutBaseScope.tasks.addAll(this.tasks)
}
/**
* Specifies the constraints associated to the layout identified with [ref].
*/
fun constrain(
ref: ConstrainedLayoutReference,
constrainBlock: ConstrainScope.() -> Unit
): ConstrainScope = ConstrainScope(ref.id).apply {
constrainBlock()
this@ConstraintLayoutBaseScope.tasks.addAll(this.tasks)
}
/**
* Creates a guideline at a specific offset from the start of the [ConstraintLayout].
*/
fun createGuidelineFromStart(offset: Dp): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
state.verticalGuideline(id).apply {
if (state.layoutDirection == LayoutDirection.Ltr) start(offset) else end(offset)
}
}
updateHelpersHashCode(1)
updateHelpersHashCode(offset.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a specific offset from the left of the [ConstraintLayout].
*/
fun createGuidelineFromAbsoluteLeft(offset: Dp): VerticalAnchor {
val id = createHelperId()
tasks.add { state -> state.verticalGuideline(id).apply { start(offset) } }
updateHelpersHashCode(2)
updateHelpersHashCode(offset.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a specific offset from the start of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the start of the [ConstraintLayout], while 1f will
* correspond to the end.
*/
fun createGuidelineFromStart(fraction: Float): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
state.verticalGuideline(id).apply {
if (state.layoutDirection == LayoutDirection.Ltr) {
percent(fraction)
} else {
percent(1f - fraction)
}
}
}
updateHelpersHashCode(3)
updateHelpersHashCode(fraction.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a width fraction from the left of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the left of the [ConstraintLayout], while 1f will
* correspond to the right.
*/
// TODO(popam, b/157781990): this is not really percenide
fun createGuidelineFromAbsoluteLeft(fraction: Float): VerticalAnchor {
val id = createHelperId()
tasks.add { state -> state.verticalGuideline(id).apply { percent(fraction) } }
updateHelpersHashCode(4)
updateHelpersHashCode(fraction.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a specific offset from the end of the [ConstraintLayout].
*/
fun createGuidelineFromEnd(offset: Dp): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
state.verticalGuideline(id).apply {
if (state.layoutDirection == LayoutDirection.Ltr) end(offset) else start(offset)
}
}
updateHelpersHashCode(5)
updateHelpersHashCode(offset.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a specific offset from the right of the [ConstraintLayout].
*/
fun createGuidelineFromAbsoluteRight(offset: Dp): VerticalAnchor {
val id = createHelperId()
tasks.add { state -> state.verticalGuideline(id).apply { end(offset) } }
updateHelpersHashCode(6)
updateHelpersHashCode(offset.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a width fraction from the end of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the end of the [ConstraintLayout], while 1f will
* correspond to the start.
*/
fun createGuidelineFromEnd(fraction: Float): VerticalAnchor {
return createGuidelineFromStart(1f - fraction)
}
/**
* Creates a guideline at a width fraction from the right of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the right of the [ConstraintLayout], while 1f will
* correspond to the left.
*/
fun createGuidelineFromAbsoluteRight(fraction: Float): VerticalAnchor {
return createGuidelineFromAbsoluteLeft(1f - fraction)
}
/**
* Creates a guideline at a specific offset from the top of the [ConstraintLayout].
*/
fun createGuidelineFromTop(offset: Dp): HorizontalAnchor {
val id = createHelperId()
tasks.add { state -> state.horizontalGuideline(id).apply { start(offset) } }
updateHelpersHashCode(7)
updateHelpersHashCode(offset.hashCode())
return HorizontalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a height percenide from the top of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the top of the [ConstraintLayout], while 1f will
* correspond to the bottom.
*/
fun createGuidelineFromTop(fraction: Float): HorizontalAnchor {
val id = createHelperId()
tasks.add { state -> state.horizontalGuideline(id).apply { percent(fraction) } }
updateHelpersHashCode(8)
updateHelpersHashCode(fraction.hashCode())
return HorizontalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a specific offset from the bottom of the [ConstraintLayout].
*/
fun createGuidelineFromBottom(offset: Dp): HorizontalAnchor {
val id = createHelperId()
tasks.add { state -> state.horizontalGuideline(id).apply { end(offset) } }
updateHelpersHashCode(9)
updateHelpersHashCode(offset.hashCode())
return HorizontalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates a guideline at a height percenide from the bottom of the [ConstraintLayout].
* A [fraction] of 0f will correspond to the bottom of the [ConstraintLayout], while 1f will
* correspond to the top.
*/
fun createGuidelineFromBottom(fraction: Float): HorizontalAnchor {
return createGuidelineFromTop(1f - fraction)
}
/**
* Creates and returns a start barrier, containing the specified elements.
*/
fun createStartBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
val direction = if (state.layoutDirection == LayoutDirection.Ltr) {
SolverDirection.LEFT
} else {
SolverDirection.RIGHT
}
state.barrier(id, direction).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(10)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates and returns a left barrier, containing the specified elements.
*/
fun createAbsoluteLeftBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
state.barrier(id, SolverDirection.LEFT).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(11)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates and returns a top barrier, containing the specified elements.
*/
fun createTopBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): HorizontalAnchor {
val id = createHelperId()
tasks.add { state ->
state.barrier(id, SolverDirection.TOP).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(12)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return HorizontalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates and returns an end barrier, containing the specified elements.
*/
fun createEndBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
val direction = if (state.layoutDirection == LayoutDirection.Ltr) {
SolverDirection.RIGHT
} else {
SolverDirection.LEFT
}
state.barrier(id, direction).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(13)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates and returns a right barrier, containing the specified elements.
*/
fun createAbsoluteRightBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): VerticalAnchor {
val id = createHelperId()
tasks.add { state ->
state.barrier(id, SolverDirection.RIGHT).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(14)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return VerticalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* Creates and returns a bottom barrier, containing the specified elements.
*/
fun createBottomBarrier(
vararg elements: LayoutReference,
margin: Dp = 0.dp
): HorizontalAnchor {
val id = createHelperId()
tasks.add { state ->
state.barrier(id, SolverDirection.BOTTOM).apply {
add(*(elements.map { it.id }.toTypedArray()))
}.margin(state.convertDimension(margin))
}
updateHelpersHashCode(15)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(margin.hashCode())
return HorizontalAnchor(id, 0, LayoutReferenceImpl(id))
}
/**
* This creates a flow helper
* Flow helpers allows a long sequence of Composable widgets to wrap onto
* multiple rows or columns.
* [flowVertically] if set to true aranges the Composables from top to bottom.
* Normally they are arranged from left to right.
* [verticalGap] defines the gap between views in the y axis
* [horizontalGap] defines the gap between views in the x axis
* [maxElement] defines the maximum element on a row before it if the
* [padding] sets padding around the content
* [wrapMode] sets the way reach maxElements is handled
* Flow.WRAP_NONE (default) -- no wrap behavior,
* Flow.WRAP_CHAIN - create additional chains
* [verticalAlign] set the way elements are aligned vertically. Center is default
* [horizontalAlign] set the way elements are aligned horizontally. Center is default
* [horizontalFlowBias] set the way elements are aligned vertically Center is default
* [verticalFlowBias] sets the top bottom bias of the vertical chain
* [verticalStyle] sets the style of a vertical chain (Spread,Packed, or SpreadInside)
* [horizontalStyle] set the style of the horizontal chain (Spread, Packed, or SpreadInside)
*/
fun createFlow(
vararg elements: LayoutReference?,
flowVertically: Boolean = false,
verticalGap: Dp = 0.dp,
horizontalGap: Dp = 0.dp,
maxElement: Int = 0,
padding: Dp = 0.dp,
wrapMode: Wrap = Wrap.None,
verticalAlign: VerticalAlign = VerticalAlign.Center,
horizontalAlign: HorizontalAlign = HorizontalAlign.Center,
horizontalFlowBias: Float = 0.0f,
verticalFlowBias: Float = 0.0f,
verticalStyle: FlowStyle = FlowStyle.Packed,
horizontalStyle: FlowStyle = FlowStyle.Packed,
): ConstrainedLayoutReference {
val id = createHelperId()
tasks.add { state ->
state.getFlow(id, flowVertically).apply {
add(*(elements.map { it?.id }.toTypedArray()))
horizontalChainStyle = horizontalStyle.style
setVerticalChainStyle(verticalStyle.style)
verticalBias(verticalFlowBias)
horizontalBias(horizontalFlowBias)
setHorizontalAlign(horizontalAlign.mode)
setVerticalAlign(verticalAlign.mode)
setWrapMode(wrapMode.mode)
paddingLeft = state.convertDimension(padding)
paddingTop = state.convertDimension(padding)
paddingRight = state.convertDimension(padding)
paddingBottom = state.convertDimension(padding)
maxElementsWrap = maxElement
setHorizontalGap(state.convertDimension(horizontalGap))
setVerticalGap(state.convertDimension(verticalGap))
}
}
updateHelpersHashCode(16)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
return ConstrainedLayoutReference(id)
}
/**
* This creates a flow helper
* Flow helpers allows a long sequence of Composable widgets to wrap onto
* multiple rows or columns.
* [flowVertically] if set to true aranges the Composables from top to bottom.
* Normally they are arranged from left to right.
* [verticalGap] defines the gap between views in the y axis
* [horizontalGap] defines the gap between views in the x axis
* [maxElement] defines the maximum element on a row before it if the
* [paddingHorizontal] sets paddingLeft and paddingRight of the content
* [paddingVertical] sets paddingTop and paddingBottom of the content
* [wrapMode] sets the way reach maxElements is handled
* Flow.WRAP_NONE (default) -- no wrap behavior,
* Flow.WRAP_CHAIN - create additional chains
* [verticalAlign] set the way elements are aligned vertically. Center is default
* [horizontalAlign] set the way elements are aligned horizontally. Center is default
* [horizontalFlowBias] set the way elements are aligned vertically Center is default
* [verticalFlowBias] sets the top bottom bias of the vertical chain
* [verticalStyle] sets the style of a vertical chain (Spread,Packed, or SpreadInside)
* [horizontalStyle] set the style of the horizontal chain (Spread, Packed, or SpreadInside)
*/
fun createFlow(
vararg elements: LayoutReference?,
flowVertically: Boolean = false,
verticalGap: Dp = 0.dp,
horizontalGap: Dp = 0.dp,
maxElement: Int = 0,
paddingHorizontal: Dp = 0.dp,
paddingVertical: Dp = 0.dp,
wrapMode: Wrap = Wrap.None,
verticalAlign: VerticalAlign = VerticalAlign.Center,
horizontalAlign: HorizontalAlign = HorizontalAlign.Center,
horizontalFlowBias: Float = 0.0f,
verticalFlowBias: Float = 0.0f,
verticalStyle: FlowStyle = FlowStyle.Packed,
horizontalStyle: FlowStyle = FlowStyle.Packed,
): ConstrainedLayoutReference {
val id = createHelperId()
tasks.add { state ->
state.getFlow(id, flowVertically).apply {
add(*(elements.map { it?.id }.toTypedArray()))
horizontalChainStyle = horizontalStyle.style
setVerticalChainStyle(verticalStyle.style)
verticalBias(verticalFlowBias)
horizontalBias(horizontalFlowBias)
setHorizontalAlign(horizontalAlign.mode)
setVerticalAlign(verticalAlign.mode)
setWrapMode(wrapMode.mode)
paddingLeft = state.convertDimension(paddingHorizontal)
paddingTop = state.convertDimension(paddingVertical)
paddingRight = state.convertDimension(paddingHorizontal)
paddingBottom = state.convertDimension(paddingVertical)
maxElementsWrap = maxElement
setHorizontalGap(state.convertDimension(horizontalGap))
setVerticalGap(state.convertDimension(verticalGap))
}
}
updateHelpersHashCode(16)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
return ConstrainedLayoutReference(id)
}
/**
* This creates a flow helper
* Flow helpers allows a long sequence of Composable widgets to wrap onto
* multiple rows or columns.
* [flowVertically] if set to true aranges the Composables from top to bottom.
* Normally they are arranged from left to right.
* [verticalGap] defines the gap between views in the y axis
* [horizontalGap] defines the gap between views in the x axis
* [maxElement] defines the maximum element on a row before it if the
* [paddingLeft] sets paddingLeft of the content
* [paddingTop] sets paddingTop of the content
* [paddingRight] sets paddingRight of the content
* [paddingBottom] sets paddingBottom of the content
* [wrapMode] sets the way reach maxElements is handled
* Flow.WRAP_NONE (default) -- no wrap behavior,
* Flow.WRAP_CHAIN - create additional chains
* [verticalAlign] set the way elements are aligned vertically. Center is default
* [horizontalAlign] set the way elements are aligned horizontally. Center is default
* [horizontalFlowBias] set the way elements are aligned vertically Center is default
* [verticalFlowBias] sets the top bottom bias of the vertical chain
* [verticalStyle] sets the style of a vertical chain (Spread,Packed, or SpreadInside)
* [horizontalStyle] set the style of the horizontal chain (Spread, Packed, or SpreadInside)
*/
fun createFlow(
vararg elements: LayoutReference?,
flowVertically: Boolean = false,
verticalGap: Dp = 0.dp,
horizontalGap: Dp = 0.dp,
maxElement: Int = 0,
paddingLeft: Dp = 0.dp,
paddingTop: Dp = 0.dp,
paddingRight: Dp = 0.dp,
paddingBottom: Dp = 0.dp,
wrapMode: Wrap = Wrap.None,
verticalAlign: VerticalAlign = VerticalAlign.Center,
horizontalAlign: HorizontalAlign = HorizontalAlign.Center,
horizontalFlowBias: Float = 0.0f,
verticalFlowBias: Float = 0.0f,
verticalStyle: FlowStyle = FlowStyle.Packed,
horizontalStyle: FlowStyle = FlowStyle.Packed,
): ConstrainedLayoutReference {
val id = createHelperId()
tasks.add { state ->
state.getFlow(id, flowVertically).apply {
add(*(elements.map { it?.id }.toTypedArray()))
horizontalChainStyle = horizontalStyle.style
setVerticalChainStyle(verticalStyle.style)
verticalBias(verticalFlowBias)
horizontalBias(horizontalFlowBias)
setHorizontalAlign(horizontalAlign.mode)
setVerticalAlign(verticalAlign.mode)
setWrapMode(wrapMode.mode)
setPaddingLeft(state.convertDimension(paddingLeft))
setPaddingTop(state.convertDimension(paddingTop))
setPaddingRight(state.convertDimension(paddingRight))
setPaddingBottom(state.convertDimension(paddingBottom))
maxElementsWrap = maxElement
setHorizontalGap(state.convertDimension(horizontalGap))
setVerticalGap(state.convertDimension(verticalGap))
}
}
updateHelpersHashCode(16)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
return ConstrainedLayoutReference(id)
}
/**
* Creates a horizontal chain including the referenced layouts.
*
* Use [constrain] with the resulting [HorizontalChainReference] to modify the start/left and
* end/right constraints of this chain.
*/
fun createHorizontalChain(
vararg elements: LayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
): HorizontalChainReference {
val id = createHelperId()
tasks.add { state ->
val helper = state.helper(
id,
androidx.constraintlayout.core.state.State.Helper.HORIZONTAL_CHAIN
) as androidx.constraintlayout.core.state.helpers.HorizontalChainReference
elements.forEach { chainElement ->
val elementParams = chainElement.getHelperParams() ?: ChainParams.Default
helper.addChainElement(
chainElement.id,
elementParams.weight,
state.convertDimension(elementParams.startMargin).toFloat(),
state.convertDimension(elementParams.endMargin).toFloat(),
state.convertDimension(elementParams.startGoneMargin).toFloat(),
state.convertDimension(elementParams.endGoneMargin).toFloat()
)
}
helper.style(chainStyle.style)
helper.apply()
if (chainStyle.bias != null) {
state.constraints(elements[0].id).horizontalBias(chainStyle.bias)
}
}
updateHelpersHashCode(16)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(chainStyle.hashCode())
return HorizontalChainReference(id)
}
/**
* Creates a vertical chain including the referenced layouts.
*
* Use [constrain] with the resulting [VerticalChainReference] to modify the top and
* bottom constraints of this chain.
*/
fun createVerticalChain(
vararg elements: LayoutReference,
chainStyle: ChainStyle = ChainStyle.Spread
): VerticalChainReference {
val id = createHelperId()
tasks.add { state ->
val helper = state.helper(
id,
androidx.constraintlayout.core.state.State.Helper.VERTICAL_CHAIN
) as androidx.constraintlayout.core.state.helpers.VerticalChainReference
elements.forEach { chainElement ->
val elementParams = chainElement.getHelperParams() ?: ChainParams.Default
helper.addChainElement(
chainElement.id,
elementParams.weight,
state.convertDimension(elementParams.topMargin).toFloat(),
state.convertDimension(elementParams.bottomMargin).toFloat(),
state.convertDimension(elementParams.topGoneMargin).toFloat(),
state.convertDimension(elementParams.bottomGoneMargin).toFloat()
)
}
helper.style(chainStyle.style)
helper.apply()
if (chainStyle.bias != null) {
state.constraints(elements[0].id).verticalBias(chainStyle.bias)
}
}
updateHelpersHashCode(17)
elements.forEach { updateHelpersHashCode(it.hashCode()) }
updateHelpersHashCode(chainStyle.hashCode())
return VerticalChainReference(id)
}
/**
* Sets the parameters that are used by chains to customize the resulting layout.
*
* Use margins to customize the space between widgets in the chain.
*
* Use weight to distribute available space to each widget when their dimensions are not
* fixed.
*
*
*
* Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
* Chains.
*
* Since margins are only for widgets within the chain: Top, Start and End, Bottom margins are
* ignored when the widget is the first or the last element in the chain, respectively.
*
* @param startMargin Added space from the start of this widget to the previous widget
* @param topMargin Added space from the top of this widget to the previous widget
* @param endMargin Added space from the end of this widget to the next widget
* @param bottomMargin Added space from the bottom of this widget to the next widget
* @param startGoneMargin Added space from the start of this widget when the previous widget has [Visibility.Gone]
* @param topGoneMargin Added space from the top of this widget when the previous widget has [Visibility.Gone]
* @param endGoneMargin Added space from the end of this widget when the next widget has [Visibility.Gone]
* @param bottomGoneMargin Added space from the bottom of this widget when the next widget has [Visibility.Gone]
* @param weight Defines the proportion of space (relative to the total weight) occupied by this
* layout when the corresponding dimension is not a fixed value.
* @return The same [LayoutReference] instance with the applied values
*/
fun LayoutReference.withChainParams(
startMargin: Dp = 0.dp,
topMargin: Dp = 0.dp,
endMargin: Dp = 0.dp,
bottomMargin: Dp = 0.dp,
startGoneMargin: Dp = 0.dp,
topGoneMargin: Dp = 0.dp,
endGoneMargin: Dp = 0.dp,
bottomGoneMargin: Dp = 0.dp,
weight: Float = Float.NaN,
): LayoutReference =
this.apply {
setHelperParams(
ChainParams(
startMargin = startMargin,
topMargin = topMargin,
endMargin = endMargin,
bottomMargin = bottomMargin,
startGoneMargin = startGoneMargin,
topGoneMargin = topGoneMargin,
endGoneMargin = endGoneMargin,
bottomGoneMargin = bottomGoneMargin,
weight = weight
)
)
}
/**
* Sets the parameters that are used by horizontal chains to customize the resulting layout.
*
* Use margins to customize the space between widgets in the chain.
*
* Use weight to distribute available space to each widget when their horizontal dimension is
* not fixed.
*
*
*
* Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
* Chains.
*
* Since margins are only for widgets within the chain: Start and End margins are
* ignored when the widget is the first or the last element in the chain, respectively.
*
* @param startMargin Added space from the start of this widget to the previous widget
* @param endMargin Added space from the end of this widget to the next widget
* @param startGoneMargin Added space from the start of this widget when the previous widget has [Visibility.Gone]
* @param endGoneMargin Added space from the end of this widget when the next widget has [Visibility.Gone]
* @param weight Defines the proportion of space (relative to the total weight) occupied by this
* layout when the width is not a fixed dimension.
* @return The same [LayoutReference] instance with the applied values
*/
fun LayoutReference.withHorizontalChainParams(
startMargin: Dp = 0.dp,
endMargin: Dp = 0.dp,
startGoneMargin: Dp = 0.dp,
endGoneMargin: Dp = 0.dp,
weight: Float = Float.NaN
): LayoutReference =
withChainParams(
startMargin = startMargin,
topMargin = 0.dp,
endMargin = endMargin,
bottomMargin = 0.dp,
startGoneMargin = startGoneMargin,
topGoneMargin = 0.dp,
endGoneMargin = endGoneMargin,
bottomGoneMargin = 0.dp,
weight = weight
)
/**
* Sets the parameters that are used by vertical chains to customize the resulting layout.
*
* Use margins to customize the space between widgets in the chain.
*
* Use weight to distribute available space to each widget when their vertical dimension is not
* fixed.
*
*
*
* Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
* Chains.
*
* Since margins are only for widgets within the chain: Top and Bottom margins are
* ignored when the widget is the first or the last element in the chain, respectively.
*
* @param topMargin Added space from the top of this widget to the previous widget
* @param bottomMargin Added space from the bottom of this widget to the next widget
* @param topGoneMargin Added space from the top of this widget when the previous widget has [Visibility.Gone]
* @param bottomGoneMargin Added space from the bottom of this widget when the next widget has [Visibility.Gone]
* @param weight Defines the proportion of space (relative to the total weight) occupied by this
* layout when the height is not a fixed dimension.
* @return The same [LayoutReference] instance with the applied values
*/
fun LayoutReference.withVerticalChainParams(
topMargin: Dp = 0.dp,
bottomMargin: Dp = 0.dp,
topGoneMargin: Dp = 0.dp,
bottomGoneMargin: Dp = 0.dp,
weight: Float = Float.NaN
): LayoutReference =
withChainParams(
startMargin = 0.dp,
topMargin = topMargin,
endMargin = 0.dp,
bottomMargin = bottomMargin,
startGoneMargin = 0.dp,
topGoneMargin = topGoneMargin,
endGoneMargin = 0.dp,
bottomGoneMargin = bottomGoneMargin,
weight = weight
)
}
/**
* Represents a [ConstraintLayout] item that requires a unique identifier. Typically a layout or a
* helper such as barriers, guidelines or chains.
*/
@Stable
abstract class LayoutReference internal constructor(internal open val id: Any) {
/**
* This map should be used to store one instance of different implementations of [HelperParams].
*/
private val helperParamsMap: MutableMap<String, HelperParams> = mutableMapOf()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LayoutReference
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
internal fun setHelperParams(helperParams: HelperParams) {
// Use the class name to force one instance per implementation
helperParamsMap[helperParams.javaClass.simpleName] = helperParams
}
/**
* Returns the [HelperParams] that corresponds to the class type [T]. Null if no instance of
* type [T] has been set.
*/
internal inline fun <reified T> getHelperParams(): T? where T : HelperParams {
return helperParamsMap[T::class.java.simpleName] as? T
}
}
/**
* Helpers that need parameters on a per-widget basis may implement this interface to store custom
* parameters within [LayoutReference].
*
* @see [LayoutReference.getHelperParams]
* @see [LayoutReference.setHelperParams]
*/
internal interface HelperParams
/**
* Parameters that may be defined for each widget within a chain.
*
* These will always be used instead of similarly named parameters defined with other calls such as
* [ConstrainScope.linkTo].
*/
internal class ChainParams(
val startMargin: Dp,
val topMargin: Dp,
val endMargin: Dp,
val bottomMargin: Dp,
val startGoneMargin: Dp,
val topGoneMargin: Dp,
val endGoneMargin: Dp,
val bottomGoneMargin: Dp,
val weight: Float,
) : HelperParams {
companion object {
internal val Default = ChainParams(
startMargin = 0.dp,
topMargin = 0.dp,
endMargin = 0.dp,
bottomMargin = 0.dp,
startGoneMargin = 0.dp,
topGoneMargin = 0.dp,
endGoneMargin = 0.dp,
bottomGoneMargin = 0.dp,
weight = Float.NaN
)
}
}
/**
* Basic implementation of [LayoutReference], used as fallback for items that don't fit other
* implementations of [LayoutReference], such as [ConstrainedLayoutReference].
*/
@Stable
internal class LayoutReferenceImpl internal constructor(id: Any) : LayoutReference(id)
/**
* Represents a layout within a [ConstraintLayout].
*
* This is a [LayoutReference] that may be constrained to other elements.
*/
@Stable
class ConstrainedLayoutReference(override val id: Any) : LayoutReference(id) {
/**
* The start anchor of this layout. Represents left in LTR layout direction, or right in RTL.
*/
@Stable
val start = ConstraintLayoutBaseScope.VerticalAnchor(id, -2, this)
/**
* The left anchor of this layout.
*/
@Stable
val absoluteLeft = ConstraintLayoutBaseScope.VerticalAnchor(id, 0, this)
/**
* The top anchor of this layout.
*/
@Stable
val top = ConstraintLayoutBaseScope.HorizontalAnchor(id, 0, this)
/**
* The end anchor of this layout. Represents right in LTR layout direction, or left in RTL.
*/
@Stable
val end = ConstraintLayoutBaseScope.VerticalAnchor(id, -1, this)
/**
* The right anchor of this layout.
*/
@Stable
val absoluteRight = ConstraintLayoutBaseScope.VerticalAnchor(id, 1, this)
/**
* The bottom anchor of this layout.
*/
@Stable
val bottom = ConstraintLayoutBaseScope.HorizontalAnchor(id, 1, this)
/**
* The baseline anchor of this layout.
*/
@Stable
val baseline = ConstraintLayoutBaseScope.BaselineAnchor(id, this)
}
/**
* Represents a horizontal chain within a [ConstraintLayout].
*
* The anchors correspond to the first and last elements in the chain.
*/
@Stable
class HorizontalChainReference internal constructor(id: Any) : LayoutReference(id) {
/**
* The start anchor of the first element in the chain.
*
* Represents left in LTR layout direction, or right in RTL.
*/
@Stable
val start = ConstraintLayoutBaseScope.VerticalAnchor(id, -2, this)
/**
* The left anchor of the first element in the chain.
*/
@Stable
val absoluteLeft = ConstraintLayoutBaseScope.VerticalAnchor(id, 0, this)
/**
* The end anchor of the last element in the chain.
*
* Represents right in LTR layout direction, or left in RTL.
*/
@Stable
val end = ConstraintLayoutBaseScope.VerticalAnchor(id, -1, this)
/**
* The right anchor of the last element in the chain.
*/
@Stable
val absoluteRight = ConstraintLayoutBaseScope.VerticalAnchor(id, 1, this)
}
/**
* Represents a vertical chain within a [ConstraintLayout].
*
* The anchors correspond to the first and last elements in the chain.
*/
@Stable
class VerticalChainReference internal constructor(id: Any) : LayoutReference(id) {
/**
* The top anchor of the first element in the chain.
*/
@Stable
val top = ConstraintLayoutBaseScope.HorizontalAnchor(id, 0, this)
/**
* The bottom anchor of the last element in the chain.
*/
@Stable
val bottom = ConstraintLayoutBaseScope.HorizontalAnchor(id, 1, this)
}
/**
* The style of a horizontal or vertical chain.
*/
@Immutable
class ChainStyle internal constructor(
internal val style: SolverChain,
internal val bias: Float? = null
) {
companion object {
/**
* A chain style that evenly distributes the contained layouts.
*/
@Stable
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.
*/
@Stable
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.
*/
@Stable
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].
*/
@Stable
fun Packed(bias: Float) = ChainStyle(SolverChain.PACKED, bias)
}
}
/**
* The overall visibility of a widget in a [ConstraintLayout].
*/
@Immutable
class Visibility internal constructor(
internal val solverValue: Int
) {
companion object {
/**
* Indicates that the widget will be painted in the [ConstraintLayout]. All render-time
* transforms will apply normally.
*/
@Stable
val Visible = Visibility(ConstraintWidget.VISIBLE)
/**
* The widget will not be painted in the [ConstraintLayout] but its dimensions and constraints
* will still apply.
*
* Equivalent to forcing the alpha to 0.0.
*/
@Stable
val Invisible = Visibility(ConstraintWidget.INVISIBLE)
/**
* Like [Invisible], but the dimensions of the widget will collapse to (0,0), the
* constraints will still apply.
*/
@Stable
val Gone = Visibility(ConstraintWidget.GONE)
}
}
/**
* Wrap defines the type of chain
*/
@Immutable
class Wrap internal constructor(
internal val mode: Int
) {
companion object {
val None =
Wrap(androidx.constraintlayout.core.widgets.Flow.WRAP_NONE)
val Chain =
Wrap(androidx.constraintlayout.core.widgets.Flow.WRAP_CHAIN)
val Aligned =
Wrap(androidx.constraintlayout.core.widgets.Flow.WRAP_ALIGNED)
}
}
/**
* Defines how objects align vertically within the chain
*/
@Immutable
class VerticalAlign internal constructor(
internal val mode: Int
) {
companion object {
val Top = VerticalAlign(androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_TOP)
val Bottom =
VerticalAlign(androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_BOTTOM)
val Center =
VerticalAlign(androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_CENTER)
val Baseline =
VerticalAlign(androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_BASELINE)
}
}
/**
* Defines how objects align horizontally in the chain
*/
@Immutable
class HorizontalAlign internal constructor(
internal val mode: Int
) {
companion object {
val Start =
HorizontalAlign(androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_START)
val End = HorizontalAlign(androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_END)
val Center =
HorizontalAlign(androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_CENTER)
}
}
/**
* Defines how widgets are spaced in a chain
*/
@Immutable
class FlowStyle internal constructor(
internal val style: Int
) {
companion object {
val Spread = FlowStyle(0)
val SpreadInside = FlowStyle(1)
val Packed = FlowStyle(2)
}
}