androidx.constraintlayout.compose
Interfaces
BaselineAnchorable |
Represents the |
ConstraintLayoutTagParentData |
|
ConstraintSet |
Immutable description of the constraints used to layout the children of a |
DesignInfoProvider |
Interface used for Studio tooling. |
Dimension |
Represents a dimension that can be assigned to the width or height of a child |
Dimension.Coercible |
A |
Dimension.MaxCoercible |
A |
Dimension.MinCoercible |
A |
HorizontalAnchorable |
Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using |
LayoutInformationReceiver |
|
MotionCarouselScope |
|
MotionItemsProvider |
|
MotionScene |
Information for MotionLayout to animate between multiple |
Transition |
Defines interpolation parameters between two |
VerticalAnchorable |
Represents a vertical side of a layout (i.e start and end) that can be anchored using |
Classes
Arc |
Determines a specific arc direction of the widget's path on a |
BaseKeyFrameScope |
The base/common scope for individual KeyFrame declarations. |
BaseKeyFramesScope |
The base/common scope for KeyFrames. |
ChainStyle |
The style of a horizontal or vertical chain. |
ConstrainScope |
Scope that can be used to constrain a layout. |
ConstrainedLayoutReference |
Represents a layout within a |
ConstraintLayoutBaseScope |
Common scope for |
ConstraintLayoutBaseScope.BaselineAnchor |
Represents a horizontal anchor corresponding to the |
ConstraintLayoutBaseScope.HorizontalAnchor |
Represents a horizontal anchor (e.g. top/bottom of a layout, guideline) that layouts can link to in their |
ConstraintLayoutBaseScope.VerticalAnchor |
Represents a vertical anchor (e.g. start/end of a layout, guideline) that layouts can link to in their |
ConstraintLayoutScope |
Scope used by the inline DSL of |
ConstraintLayoutScope.ConstrainedLayoutReferences |
Convenience API for creating multiple |
ConstraintSetRef |
|
ConstraintSetScope |
Scope used by the |
ConstraintSetScope.ConstrainedLayoutReferences |
|
CurveFit |
Type of fit applied between curves. |
DebugFlags |
Flags to use with MotionLayout to enable visual debugging. |
Easing |
Supported Easing curves. |
FlowStyle |
Defines how widgets are spaced in a chain |
GridFlag |
Set of individual options that may change the Grid helper behavior, each flag can be combined with the |
HorizontalAlign |
Defines how objects align horizontally in the chain |
HorizontalChainReference |
Represents a horizontal chain within a |
HorizontalChainScope |
|
InvalidationStrategy |
Provide different invalidation strategies for |
InvalidationStrategySpecification |
Helper scope that provides some strategies to improve performance based on incoming constraints. |
KeyAttributeScope |
Scope to define KeyFrame attributes. |
KeyAttributesScope |
Scope where multiple attribute KeyFrames may be defined. |
KeyCycleScope |
Scope to define cycling KeyFrames. |
KeyCyclesScope |
Scope where multiple cycling attribute KeyFrames may be defined. |
KeyPositionScope |
Scope to define KeyFrame positions. |
KeyPositionsScope |
Scope where multiple position KeyFrames may be defined. |
LayoutReference |
Represents a |
MotionLayoutScope |
|
MotionLayoutScope.CustomProperties |
|
MotionLayoutScope.MotionProperties |
|
MotionSceneScope |
Scope used by the MotionScene DSL. |
MotionSceneScope.ConstrainedLayoutReferences |
|
OnSwipe |
Defines the OnSwipe behavior for a |
RelativePosition |
Relative coordinate space in which KeyPositions are applied. |
Skip |
Defines how many rows and/or columns to skip, starting from the given position. |
Span |
Defines the spanned area (that crosses multiple columns and/or rows) that a widget will take when placed at the given position. |
SpringBoundary |
Behavior of the spring as it crosses its target position. |
State |
The state of the |
SwipeDirection |
Direction of the touch input that will initiate the swipe handling. |
SwipeMode |
Defines the type of motion used when animating during touch-up. |
SwipeSide |
Side of the bounds to track during touch handling, this is to account for when the widget changes size during the |
SwipeTouchUp |
The logic used to decide the target position when the touch input ends. |
TransitionScope |
Scope where |
VerticalAlign |
Defines how objects align vertically within the chain |
VerticalChainReference |
Represents a vertical chain within a |
VerticalChainScope |
|
Visibility |
The overall visibility of a widget in a |
Wrap |
Wrap defines the type of chain |
Objects
Annotations
Enums
LayoutInfoFlags |
|
MotionLayoutDebugFlags |
|
MotionLayoutFlag |
This enum is deprecated. Unnecessary, MotionLayout remeasures when its content changes. |
Top-level functions summary
inline Unit |
@Composable Layout that positions its children according to the constraints between them. |
inline Unit |
@Composable Layout that positions its children according to the constraints between them. |
inline Unit |
@Composable This function is deprecated. Prefer version that takes a nullable AnimationSpec to animate changes. |
inline Unit |
@Composable This function is deprecated. Prefer version that takes a nullable AnimationSpec to animate changes. |
ConstraintSet |
ConstraintSet(description: ConstraintSetScope.() -> Unit) Creates a |
ConstraintSet |
ConstraintSet(@Language(value = "json5") jsonContent: String) Parses the given JSON5 into a |
ConstraintSet |
@Composable Parses |
ConstraintSet |
ConstraintSet(extendConstraintSet: ConstraintSet, description: ConstraintSetScope.() -> Unit) Creates a |
ConstraintSet |
ConstraintSet( Creates a |
Unit |
@Composable |
Unit |
@Composable Implements an horizontal Carousel of n elements, driven by drag gestures and customizable through a provided MotionScene. |
inline Unit |
@ExperimentalMotionApi Layout that can animate between multiple |
inline Unit |
@ExperimentalMotionApi Layout that can animate between multiple |
inline Unit |
@ExperimentalMotionApi Layout that can animate between two different layout states described in |
MotionScene |
@ExperimentalMotionApi Parses the given JSON5 into a |
MotionScene |
@ExperimentalMotionApi Returns a |
Transition |
@ExperimentalMotionApi Parses the given JSON5 into a |
Transition |
@ExperimentalMotionApi Defines the interpolation parameters between the |
Extension functions summary
Dimension.MaxCoercible |
Dimension.Coercible.atLeast(dp: Dp) Sets the lower bound of the current |
Dimension |
Sets the lower bound of the current |
Dimension |
This function is deprecated. Unintended method name, use atLeast(dp) instead |
Dimension.MinCoercible |
Dimension.Coercible.atMost(dp: Dp) Sets the upper bound of the current |
Dimension |
Sets the upper bound of the current |
inline Unit |
<T : Any?> MotionCarouselScope.items( |
inline Unit |
<T : Any?> MotionCarouselScope.itemsWithProperties( |
Modifier |
Alternative to |
Top-level properties summary
Extension properties summary
Dimension.MaxCoercible |
Sets the lower bound of the current |
Dimension |
Sets the lower bound of the current |
Dimension.MinCoercible |
Sets the upper bound of the current |
Dimension |
Sets the upper bound of the current |
Any? |
|
Any? |
Top-level functions
ConstraintLayout
@Composable
inline fun ConstraintLayout(
modifier: Modifier = Modifier,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
animateChangesSpec: AnimationSpec<Float>? = null,
noinline finishedAnimationListener: (() -> Unit)? = null,
crossinline content: @Composable ConstraintLayoutScope.() -> Unit
): Unit
Layout that positions its children according to the constraints between them.
Constraints are defined within the content of this ConstraintLayout Composable
.
Items in the layout that are to be constrained are initialized with ConstraintLayoutScope.createRef
:
val textRef = createRef()
val imageRef = createRef()
You may also use ConstraintLayoutScope.createRefs
to declare up to 16 items using the destructuring declaration pattern:
val (textRef, imageRef) = createRefs()
Individual constraints are defined with Modifier.constrainAs
, this will also bind the Composable to the given ConstrainedLayoutReference
.
So, a simple layout with a text in the middle and an image next to it may be declared like this (keep in mind, when using center...
, start
or end
the layout direction will automatically change in RTL locales):
ConstraintLayout(Modifier.fillMaxSize()) {
val (textRef, imageRef) = createRefs()
Text(
modifier = Modifier.constrainAs(textRef) {
centerTo(parent)
},
text = "Hello, World!"
)
Image(
modifier = Modifier.constrainAs(imageRef) {
centerVerticallyTo(textRef)
start.linkTo(textRef.end, margin = 8.dp)
},
imageVector = Icons.Default.Android,
contentDescription = null
)
}
See ConstrainScope
to learn more about how to constrain elements together.
Helpers
You may also use helpers, a set of virtual (not shown on screen) components that provide special layout behaviors, you may find these in the ConstraintLayoutScope
with the 'create...
' prefix, a few of these are Guidelines, Chains and Barriers.
Guidelines
Lines to which other ConstrainedLayoutReference
s may be constrained to, these are defined at either a fixed or percent position from an anchor of the ConstraintLayout parent (top, bottom, start, end, absoluteLeft, absoluteRight).
Example:
val (textRef) = createRefs()
val vG = createGuidelineFromStart(fraction = 0.3f)
Text(
modifier = Modifier.constrainAs(textRef) {
centerVerticallyTo(parent)
centerAround(vG)
},
text = "Hello, World!"
)
See
Chains
Chains may be either horizontal or vertical, these, take a set of ConstrainedLayoutReference
s and create bi-directional constraints on each of them at the same orientation of the chain in the given order, meaning that an horizontal chain will create constraints between the start and end anchors.
The result, a layout that evenly distributes the space within its elements.
For example, to make a layout with three text elements distributed so that the spacing between them (and around them) is equal:
val (textRef0, textRef1, textRef2) = createRefs()
createHorizontalChain(textRef0, textRef1, textRef2, chainStyle = ChainStyle.Spread)
Text(modifier = Modifier.constrainAs(textRef0) {}, text = "Hello")
Text(modifier = Modifier.constrainAs(textRef1) {}, text = "Foo")
Text(modifier = Modifier.constrainAs(textRef2) {}, text = "Bar")
You may set margins within elements in a chain with ConstraintLayoutScope.withChainParams
:
val (textRef0, textRef1, textRef2) = createRefs()
createHorizontalChain(
textRef0,
textRef1.withChainParams(startMargin = 100.dp, endMargin = 100.dp),
textRef2,
chainStyle = ChainStyle.Spread
)
Text(modifier = Modifier.constrainAs(textRef0) {}, text = "Hello")
Text(modifier = Modifier.constrainAs(textRef1) {}, text = "Foo")
Text(modifier = Modifier.constrainAs(textRef2) {}, text = "Bar")
You can also change the way space is distributed, as chains have three different styles:
-
ChainStyle.Spread
Layouts are evenly distributed after margins are accounted for (the space around and between each item is even). This is the default style for chains. -
ChainStyle.SpreadInside
The first and last layouts are affixed to each end of the chain, and the rest of the items are evenly distributed (after margins are accounted for). I.e.: Items are spread from the inside, distributing the space between them with no space around the first and last items. -
ChainStyle.Packed
The layouts are packed together after margins are accounted for, by default, they're packed together at the middle, you can change this behavior with the bias parameter ofChainStyle.Packed
. -
Alternatively, you can make every Layout in the chain to be
Dimension.fillToConstraints
and then set a particular weight to each of them to create a weighted chain.
Weighted Chain
Weighted chains are useful when you want the size of the elements to depend on the remaining size of the chain. As opposed to just distributing the space around and/or in-between the items.
For example, to create a layout with three text elements in a row where each element takes the exact same size regardless of content, you can use a simple weighted chain where each item has the same weight:
val (textRef0, textRef1, textRef2) = createRefs()
createHorizontalChain(
textRef0.withChainParams(weight = 1f),
textRef1.withChainParams(weight = 1f),
textRef2.withChainParams(weight = 1f),
chainStyle = ChainStyle.Spread
)
Text(modifier = Modifier.background(Color.Cyan).constrainAs(textRef0) {
width = Dimension.fillToConstraints
}, text = "Hello, World!")
Text(modifier = Modifier.background(Color.Red).constrainAs(textRef1) {
width = Dimension.fillToConstraints
}, text = "Foo")
Text(modifier = Modifier.background(Color.Cyan).constrainAs(textRef2) {
width = Dimension.fillToConstraints
}, text = "This text is six words long")
This way, the texts will horizontally occupy the same space even if one of them is significantly larger than the others.
Keep in mind that chains have a relatively high performance cost. For example, if you plan on having multiple chains one below the other, consider instead, applying just one chain and using it as a reference to constrain all other elements to the ones that match their position in that one chain. It may provide increased performance with no significant changes in the layout output.
Alternatively, consider if other helpers such as ConstraintLayoutScope.createGrid
can accomplish the same layout.
See
Barriers
Barriers take a set of ConstrainedLayoutReference
s and creates the most further point in a given direction where other ConstrainedLayoutReference
can constrain to.
This is useful in situations where elements in a layout may have different sizes but you want to always constrain to the largest item, for example, if you have a text element on top of another and want an image to always be constrained to the end of them:
val (textRef0, textRef1, imageRef) = createRefs()
// Creates a point at the furthest end anchor from the elements in the barrier
val endTextsBarrier = createEndBarrier(textRef0, textRef1)
Text(
modifier = Modifier.constrainAs(textRef0) {
centerTo(parent)
},
text = "Hello, World!"
)
Text(
modifier = Modifier.constrainAs(textRef1) {
top.linkTo(textRef0.bottom)
start.linkTo(textRef0.start)
},
text = "Foo Bar"
)
Image(
modifier = Modifier.constrainAs(imageRef) {
top.linkTo(textRef0.top)
bottom.linkTo(textRef1.bottom)
// Image will always be at the end of both texts, regardless of their size
start.linkTo(endTextsBarrier, margin = 8.dp)
},
imageVector = Icons.Default.Android,
contentDescription = null
)
Be careful not to constrain a ConstrainedLayoutReference
to a barrier that references it or that depends on it indirectly. This creates a cyclic dependency that results in unsupported layout behavior.
See
Tip: If you notice that you are creating many different constraints based on State
variables or configuration changes, consider using the ConstraintSet
pattern instead, makes it clearer to distinguish different layouts and allows you to automatically animate the layout when the provided ConstraintSet
is different.
Parameters | |
---|---|
modifier: Modifier = Modifier |
Modifier to apply to this layout node. |
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD |
Optimization flags for ConstraintLayout. The default is |
animateChangesSpec: AnimationSpec<Float>? = null |
Null by default. Otherwise, ConstraintLayout will animate the layout if there were any changes on the constraints during recomposition using the given |
noinline finishedAnimationListener: (() -> Unit)? = null |
Lambda called whenever an animation due to |
crossinline content: @Composable ConstraintLayoutScope.() -> Unit |
Content of this layout node. |
ConstraintLayout
@Composable
inline fun ConstraintLayout(
constraintSet: ConstraintSet,
modifier: Modifier = Modifier,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
animateChangesSpec: AnimationSpec<Float>? = null,
noinline finishedAnimationListener: (() -> Unit)? = null,
crossinline content: @Composable () -> Unit
): Unit
Layout that positions its children according to the constraints between them.
This Composable
of ConstraintLayout
takes a ConstraintSet
where the layout is defined using references and constraints.
Layouts referenced in the given constraintSet
can be bound to immediate child Composables using Modifier.layoutId
, where the given layoutIds match each named reference.
So, a simple layout with a text in the middle and an image next to it may be declared like this:
// IDs
val textId = "text"
val imageId = "image"
// Layout definition with references and constraints
val constraintSet = remember {
ConstraintSet {
val (textRef, imageRef) = createRefsFor(textId, imageId)
constrain(textRef) {
centerTo(parent)
}
constrain(imageRef) {
centerVerticallyTo(textRef)
start.linkTo(textRef.end, margin = 8.dp)
}
}
}
// ConstraintLayout uses our given ConstraintSet
ConstraintLayout(
constraintSet = constraintSet,
modifier = Modifier.fillMaxSize()
) {
// References are bound to Composables using Modifier.layoutId(Any)
Text(
modifier = Modifier.layoutId(textId),
text = "Hello, World!"
)
Image(
modifier = Modifier.layoutId(imageId),
imageVector = Icons.Default.Android,
contentDescription = null
)
}
See ConstraintSet
to learn more on how to declare layouts using constraints.
Handling of ConstraintSet objects
You typically want to remember
declared ConstraintSet
s, to avoid unnecessary allocations on recomposition, if the ConstraintSetScope
block consumes any State
variables, then something like remember { derivedStateOf { ConstraintSet { ... } } }
would be more appropriate.
However, note in the example above that our ConstraintSet is constant, so we can declare it at a top level, improving overall Composition performance:
private const val TEXT_ID = "text"
private const val IMAGE_ID = "image"
private val mConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
ConstraintSet {
val (textRef, imageRef) = createRefsFor(TEXT_ID, IMAGE_ID)
constrain(textRef) {
centerTo(parent)
}
constrain(imageRef) {
centerVerticallyTo(textRef)
start.linkTo(textRef.end, margin = 8.dp)
}
}
}
@Preview
@Composable
fun ConstraintSetExample() {
ConstraintLayout(
constraintSet = mConstraintSet,
modifier = Modifier.fillMaxSize()
) {
Text(
modifier = Modifier.layoutId(TEXT_ID),
text = "Hello, World!"
)
Image(
modifier = Modifier.layoutId(IMAGE_ID),
imageVector = Icons.Default.Android,
contentDescription = null
)
}
}
This pattern (as opposed to defining constraints with ConstraintLayoutScope.constrainAs
) is preferred when you want different layouts to be produced on different State
variables or configuration changes. As it makes it easier to create distinguishable layouts, for example when building adaptive layouts based on Window size class:
private const val NAV_BAR_ID = "navBar"
private const val CONTENT_ID = "content"
private val compactConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
ConstraintSet {
val (navBarRef, contentRef) = createRefsFor(NAV_BAR_ID, CONTENT_ID)
// Navigation bar at the bottom for Compact devices
constrain(navBarRef) {
width = Dimension.percent(1f)
height = 40.dp.asDimension()
bottom.linkTo(parent.bottom)
}
constrain(contentRef) {
width = Dimension.percent(1f)
height = Dimension.fillToConstraints
top.linkTo(parent.top)
bottom.linkTo(navBarRef.top)
}
}
}
private val mediumConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
ConstraintSet {
val (navBarRef, contentRef) = createRefsFor(NAV_BAR_ID, CONTENT_ID)
// Navigation bar at the start on Medium class devices
constrain(navBarRef) {
width = 40.dp.asDimension()
height = Dimension.percent(1f)
start.linkTo(parent.start)
}
constrain(contentRef) {
width = Dimension.fillToConstraints
height = Dimension.percent(1f)
start.linkTo(navBarRef.end)
end.linkTo(parent.end)
}
}
}
@Composable
fun MyAdaptiveLayout(
windowWidthSizeClass: WindowWidthSizeClass
) {
val constraintSet = if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
compactConstraintSet
}
else {
mediumConstraintSet
}
ConstraintLayout(
constraintSet = constraintSet,
modifier = Modifier.fillMaxSize()
) {
Box(Modifier.background(Color.Blue).layoutId(NAV_BAR_ID))
Box(Modifier.background(Color.Red).layoutId(CONTENT_ID))
}
}
Animate Changes
When using multiple discrete ConstraintSet
s, you may pass non-null object to animateChangesSpec
. With this, whenever ConstraintLayout is recomposed with a different ConstraintSet
(by equality), it will animate all its children using the given AnimationSpec
.
On the example above, using animateChangesSpec
would result on the layout being animated when the device changes to non-compact window class, typical behavior in some Foldable devices.
If more control is needed, we recommend using MotionLayout
instead, which has a very similar pattern through the MotionScene
object.
Parameters | |
---|---|
constraintSet: ConstraintSet |
The |
modifier: Modifier = Modifier |
Modifier to apply to this layout node. |
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD |
Optimization flags for ConstraintLayout. The default is |
animateChangesSpec: AnimationSpec<Float>? = null |
Null by default. Otherwise, ConstraintLayout will animate the layout if a different |
noinline finishedAnimationListener: (() -> Unit)? = null |
Lambda called whenever an animation due to |
crossinline content: @Composable () -> Unit |
Content of this layout node. |
ConstraintLayout
@Composable
inline funConstraintLayout(
modifier: Modifier = Modifier,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
animateChanges: Boolean = false,
animationSpec: AnimationSpec<Float> = tween<Float>(),
noinline finishedAnimationListener: (() -> Unit)? = null,
crossinline content: @Composable ConstraintLayoutScope.() -> Unit
): Unit
ConstraintLayout
@Composable
inline funConstraintLayout(
constraintSet: ConstraintSet,
modifier: Modifier = Modifier,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
animateChanges: Boolean = false,
animationSpec: AnimationSpec<Float> = tween<Float>(),
noinline finishedAnimationListener: (() -> Unit)? = null,
crossinline content: @Composable () -> Unit
): Unit
ConstraintSet
fun ConstraintSet(description: ConstraintSetScope.() -> Unit): ConstraintSet
Creates a ConstraintSet
with the constraints defined in the description
block.
See ConstraintSet
to learn how to define constraints.
ConstraintSet
fun ConstraintSet(@Language(value = "json5") jsonContent: String): ConstraintSet
Parses the given JSON5 into a ConstraintSet
.
See the official GitHub Wiki to learn the syntax.
ConstraintSet
@Composable
fun ConstraintSet(
@Language(value = "json5") content: String,
@Language(value = "json5") overrideVariables: String? = null
): ConstraintSet
Parses content
into a ConstraintSet
and sets the variables defined in the Variables
block with the values of overrideVariables
.
Eg:
For Variables: { margin: { from: 'initialMargin', step: 10 } }
overrideVariables = "{ 'initialMargin' = 50 }"
Will create a ConstraintSet where initialMargin
is 50.
ConstraintSet
fun ConstraintSet(extendConstraintSet: ConstraintSet, description: ConstraintSetScope.() -> Unit): ConstraintSet
Creates a ConstraintSet
that extends the changes applied by extendConstraintSet
.
See ConstraintSet
to learn how to define constraints.
ConstraintSet
fun ConstraintSet(
extendConstraintSet: ConstraintSet,
@Language(value = "json5") jsonContent: String
): ConstraintSet
Creates a ConstraintSet
from a jsonContent
string that extends the changes applied by extendConstraintSet
.
ItemHolder
@Composable
fun ItemHolder(
i: Int,
slotPrefix: String,
showSlot: Boolean,
function: @Composable () -> Unit
): Unit
MotionCarousel
@Composable
fun MotionCarousel(
motionScene: MotionScene,
initialSlotIndex: Int,
numSlots: Int,
backwardTransition: String = "backward",
forwardTransition: String = "forward",
slotPrefix: String = "slot",
showSlots: Boolean = false,
content: MotionCarouselScope.() -> Unit
): Unit
Implements an horizontal Carousel of n elements, driven by drag gestures and customizable through a provided MotionScene.
Usage
-----val cardsExample = arrayListOf(...)
MotionCarousel(motionScene...) { items(cardsExample) { card -> SomeCardComponent(card) } }
or if wanting to use parameters in your components that are defined in the MotionScene:
MotionCarousel(motionScene...) { itemsWithProperties(cardsExample) { card, properties -> SomeCardComponent(card, properties) } }
Note
----It is recommended to encapsulate the usage of MotionCarousel:
fun MyCarousel(content: MotionCarouselScope.() -> Unit) { val motionScene = ... MotionCarousel(motionScene..., content) }
Mechanism overview and MotionScene architecture
-----------------------------------------------We use 3 different states to represent the Carousel: "previous", "start", and "next". A horizontal swipe gesture will transition from one state to the other, e.g. a right to left swipe will transition from "start" to "next".
We consider a scene containing several "slots" for the elements we want to display in the Carousel. In an horizontal carousel, the easiest way to think of them is as an horizontal list of slots.
The overall mechanism thus works by moving those "slots" according to the gesture, and then mapping the Carousel's elements to the corresponding slots as we progress through the list of elements.
For example, let's consider using a Carousel with 3 slots 1 and 2, with 1 the center slot being the only visible one during the initial state "start" (| and | representing the screen borders) and 0 being outside of the screen on the left and 2 outside of the screen on the right:
start 0 | 1 | 2
We can setup the previous state in the following way:
previous | 0 | 3
And the next state like:
next 1 | 2 |
All three states together allowing to implement the Carousel motion we are looking for:
previous | 0 | 3 start 0 | 1 | 2 next 1 | 2 |
At the end of the swipe gesture, we instantly move back to the start state:
start 0 | 1 | 2 -> gesture starts next 1 | 2 | -> gesture ends start 0 | 1 | 2 -> instant snap back to start state
After the instant snap, we update the elements actually displayed in the slots. For example, we can start with the elements {a}, {b} and {c} assigned respectively to the slots 0, 1 and 2. After the swipe the slots will be reassigned to {b}, {c} and {d}:
start 0:{a} | 1:{b} | 2:{d} -> gesture starts next 0:{a} 1:{b} | 2:{c} | -> gesture ends start 0:{a} | 1:{b} | 2:{c} -> instant snap back to start state start 0:{b} | 1:{c} | 2:{d} -> repaint with reassigned elements
In this manner, the overall effect emulate an horizontal scroll of a list of elements.
A similar mechanism is applied the left to right gesture going through the previous state.
Starting slot
-------------In order to operate, we need a list of slots. We retrieve them from the motionScene by adding to the slotPrefix an index number. As the starting slot may not be the first one in the scene, we also need to be able to specify a startIndex.
Note that at the beginning of the Carousel, we will not populate the slots that have a lower index than startIndex, and at the end of the Carousel, we will not populate the slots that have a higher index than startIndex.
Parameters | |
---|---|
initialSlotIndex: Int |
the slot index that holds the current element |
numSlots: Int |
the number of slots in the scene |
backwardTransition: String = "backward" |
the name of the previous transition (default "previous") |
forwardTransition: String = "forward" |
the name of the next transition (default "next") |
slotPrefix: String = "slot" |
the prefix used for the slots widgets in the scene (default "card") |
showSlots: Boolean = false |
a debug flag to display the slots in the scene regardless if they are populated |
content: MotionCarouselScope.() -> Unit |
the MotionCarouselScope we use to map the elements to the slots |
MotionLayout
@ExperimentalMotionApi
@Composable
inline fun MotionLayout(
motionScene: MotionScene,
progress: Float,
modifier: Modifier = Modifier,
transitionName: String = "default",
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
crossinline content: @Composable MotionLayoutScope.() -> Unit
): Unit
Layout that can animate between multiple ConstraintSet
s as defined by Transition
s in the given MotionScene
.
The animation is driven by the progress
value, so it will typically be a result of using an Animatable
or animateFloatAsState
:
var animateToEnd by remember { mutableStateOf(false) }
MotionLayout(
motionScene = MotionScene {
val buttonRef = createRefFor("button")
defaultTransition(
from = constraintSet {
constrain(buttonRef) {
top.linkTo(parent.top)
}
},
to = constraintSet {
constrain(buttonRef) {
bottom.linkTo(parent.bottom)
}
}
)
},
progress = animateFloatAsState(if (animateToEnd) 1f else 0f).value,
modifier = Modifier.fillMaxSize()
) {
Button(onClick = { animateToEnd = !animateToEnd }, Modifier.layoutId("button")) {
Text("Hello, World!")
}
}
Note that you must use Modifier.layoutId
to bind the references used in the ConstraintSet
s to the Composable.
Parameters | |
---|---|
motionScene: MotionScene |
Holds all the layout states defined in |
progress: Float |
Sets the interpolated position of the layout between the ConstraintSets. |
modifier: Modifier = Modifier |
Modifier to apply to this layout node. |
transitionName: String = "default" |
The name of the transition to apply on the layout. By default, it will target the transition defined with |
debugFlags: DebugFlags = DebugFlags.None |
Flags to enable visual debugging. |
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD |
Optimization parameter for the underlying ConstraintLayout, |
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy |
Provides strategies to optimize invalidations in |
crossinline content: @Composable MotionLayoutScope.() -> Unit |
The content to be laid out by MotionLayout, note that each layout Composable should be bound to an ID defined in the |
MotionLayout
@ExperimentalMotionApi
@Composable
inline fun MotionLayout(
motionScene: MotionScene,
constraintSetName: String?,
animationSpec: AnimationSpec<Float>,
modifier: Modifier = Modifier,
noinline finishedAnimationListener: (() -> Unit)? = null,
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
crossinline content: @Composable MotionLayoutScope.() -> Unit
): Unit
Layout that can animate between multiple ConstraintSet
s as defined by Transition
s in the given MotionScene
.
The animation is driven based on the given constraintSetName
. During recomposition, MotionLayout will interpolate from whichever ConstraintSet
it currently is, to the one corresponding to constraintSetName
. So, a null constraintSetName
will result in no changes.
var name by remember { mutableStateOf(0) }
MotionLayout(
motionScene = MotionScene {
val buttonRef = createRefFor("button")
val initialStart = constraintSet("0") {
constrain(buttonRef) {
centerHorizontallyTo(parent, bias = 0f)
centerVerticallyTo(parent, bias = 0f)
}
}
val initialEnd = constraintSet("1") {
constrain(buttonRef) {
centerHorizontallyTo(parent, bias = 0f)
centerVerticallyTo(parent, bias = 1f)
}
}
constraintSet("2") {
constrain(buttonRef) {
centerHorizontallyTo(parent, bias = 1f)
centerVerticallyTo(parent, bias = 0f)
}
}
constraintSet("3") {
constrain(buttonRef) {
centerHorizontallyTo(parent, bias = 1f)
centerVerticallyTo(parent, bias = 1f)
}
}
// We need at least the default transition to define the initial state
defaultTransition(initialStart, initialEnd)
},
constraintSetName = name.toString(),
animationSpec = tween(1200),
modifier = Modifier.fillMaxSize()
) {
// Switch to a random ConstraintSet on click
Button(onClick = { name = IntRange(0, 3).random() }, Modifier.layoutId("button")) {
Text("Hello, World!")
}
}
Animations are run one after the other, if multiple are queued, only the last one will be executed. You may use finishedAnimationListener
to know whenever an animation is finished.
Parameters | |
---|---|
motionScene: MotionScene |
Holds all the layout states defined in |
constraintSetName: String? |
The name of the |
animationSpec: AnimationSpec<Float> |
Specifies how the internal progress value is animated. |
modifier: Modifier = Modifier |
Modifier to apply to this layout node. |
noinline finishedAnimationListener: (() -> Unit)? = null |
Called when an animation triggered by a change in |
debugFlags: DebugFlags = DebugFlags.None |
Flags to enable visual debugging. |
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD |
Optimization parameter for the underlying ConstraintLayout, |
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy |
Provides strategies to optimize invalidations in |
crossinline content: @Composable MotionLayoutScope.() -> Unit |
The content to be laid out by MotionLayout, note that each layout Composable should be bound to an ID defined in the |
MotionLayout
@ExperimentalMotionApi
@Composable
inline fun MotionLayout(
start: ConstraintSet,
end: ConstraintSet,
progress: Float,
modifier: Modifier = Modifier,
transition: Transition? = null,
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
crossinline content: @Composable MotionLayoutScope.() -> Unit
): Unit
Layout that can animate between two different layout states described in ConstraintSet
s.
The animation is driven by the progress
value, so it will typically be a result of using an Animatable
or animateFloatAsState
:
var animateToEnd by remember { mutableStateOf(false) }
MotionLayout(
start = ConstraintSet {
constrain(createRefFor("button")) {
top.linkTo(parent.top)
}
},
end = ConstraintSet {
constrain(createRefFor("button")) {
bottom.linkTo(parent.bottom)
}
},
progress = animateFloatAsState(if (animateToEnd) 1f else 0f).value,
modifier = Modifier.fillMaxSize()
) {
Button(onClick = { animateToEnd = !animateToEnd }, Modifier.layoutId("button")) {
Text("Hello, World!")
}
}
Note that you must use Modifier.layoutId
to bind the references used in the ConstraintSet
s to the Composable.
Parameters | |
---|---|
start: ConstraintSet |
ConstraintSet that defines the layout at 0f progress. |
end: ConstraintSet |
ConstraintSet that defines the layout at 1f progress. |
progress: Float |
Sets the interpolated position of the layout between the ConstraintSets. |
modifier: Modifier = Modifier |
Modifier to apply to this layout node. |
transition: Transition? = null |
Defines the interpolation parameters between the |
debugFlags: DebugFlags = DebugFlags.None |
Flags to enable visual debugging. |
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD |
Optimization parameter for the underlying ConstraintLayout, |
invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy |
Provides strategies to optimize invalidations in |
crossinline content: @Composable MotionLayoutScope.() -> Unit |
The content to be laid out by MotionLayout, note that each layout Composable should be bound to an ID defined in the |
MotionScene
@ExperimentalMotionApi
@Composable
fun MotionScene(@Language(value = "json5") content: String): MotionScene
Parses the given JSON5 into a MotionScene
.
See the official GitHub Wiki to learn the syntax.
MotionScene
@ExperimentalMotionApi
fun MotionScene(motionSceneContent: MotionSceneScope.() -> Unit): MotionScene
Returns a MotionScene
instance defined by motionSceneContent
.
Transition
@ExperimentalMotionApi
fun Transition(@Language(value = "json5") content: String): Transition
Parses the given JSON5 into a Transition
.
See the official GitHub Wiki to learn the syntax.
Transition
@ExperimentalMotionApi
fun Transition(from: String = "start", to: String = "end", content: TransitionScope.() -> Unit): Transition
Defines the interpolation parameters between the ConstraintSet
s to achieve fine-tuned animations.
Parameters | |
---|---|
from: String = "start" |
The name of the initial |
to: String = "end" |
The name of the target |
content: TransitionScope.() -> Unit |
Lambda to define the Transition parameters on the given |
Extension functions
atLeast
fun Dimension.Coercible.atLeast(dp: Dp): Dimension.MaxCoercible
Sets the lower bound of the current Dimension
to a fixed dp
value.
atLeast
fun Dimension.MinCoercible.atLeast(dp: Dp): Dimension
Sets the lower bound of the current Dimension
to a fixed dp
value.
atLeastWrapContent
fun Dimension.MinCoercible.atLeastWrapContent(dp: Dp): Dimension
Sets the lower bound of the current Dimension
to a fixed dp
value.
atMost
fun Dimension.Coercible.atMost(dp: Dp): Dimension.MinCoercible
Sets the upper bound of the current Dimension
to a fixed dp
value.
atMost
fun Dimension.MaxCoercible.atMost(dp: Dp): Dimension
Sets the upper bound of the current Dimension
to a fixed dp
value.
items
inline fun <T : Any?> MotionCarouselScope.items(
items: List<T>,
crossinline itemContent: @Composable (item) -> Unit
): Unit
itemsWithProperties
inline fun <T : Any?> MotionCarouselScope.itemsWithProperties(
items: List<T>,
crossinline itemContent: @Composable (item, properties: State<MotionLayoutScope.MotionProperties>) -> Unit
): Unit
layoutId
fun Modifier.layoutId(layoutId: String, tag: String? = null): Modifier
Alternative to androidx.compose.ui.layout.layoutId
that enables the use of tag
.
Parameters | |
---|---|
layoutId: String |
The unique Id string assigned to the Composable |
tag: String? = null |
A string to represent a group of Composables that may be affected by a ConstraintLayout function. Eg: The |
Top-level properties
Extension properties
atLeastWrapContent
val Dimension.Coercible.atLeastWrapContent: Dimension.MaxCoercible
Sets the lower bound of the current Dimension
to be the wrap content size of the child.
atLeastWrapContent
val Dimension.MinCoercible.atLeastWrapContent: Dimension
Sets the lower bound of the current Dimension
to be the wrap content size of the child.
atMostWrapContent
val Dimension.Coercible.atMostWrapContent: Dimension.MinCoercible
Sets the upper bound of the current Dimension
to be the wrap content size of the child.
atMostWrapContent
val Dimension.MaxCoercible.atMostWrapContent: Dimension
Sets the upper bound of the current Dimension
to be the WRAP_DIMENSION
size of the child.