-
Notifications
You must be signed in to change notification settings - Fork 174
Quick Guide to ConstraintLayout and MotionLayout in Compose
To start using ConstrainLayout and MotionLayout in Compose add constraintLayout-compose to your Gradle file:
dependencies {
implementation 'androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha08'
}
For more details about ConstraintLayout in Compose, please see:
- Introduction to ConstraintLayout in Compose
- ConstraintSet JSON5 syntax
- Compose ConstraintLayout DSL Syntax
Constraints in ConstraintLayout are used to define a layout by assigning constraints for every child view/widget relative to other views present. Constraints allow you to anchor sides of a Composable to the ConstraintLayout, Guidelines, or each other.
A typical constraint is “top.linkTo(title.bottom, 16.dp)” This says the top of this Composable is 16 dips below the bottom of the composable “title”. There are many other convenience methods constraint such as “centerVerticallyTo(parent)”
@Composable
public fun ScreenExample() {
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
) {
val (button, title) = createRefs()
val g1 = createGuidelineFromStart(80.dp)
Button(
modifier = Modifier.constrainAs(button) {
top.linkTo(title.bottom, 16.dp)
start.linkTo(g1)
},
onClick = {},
) {
Text(text = stringResource(id = R.string.log_in))
}
Text(modifier = Modifier.constrainAs(title) {
centerVerticallyTo(parent)
start.linkTo(g1)
},
text = stringResource(id = R.string.welcome_header),
style = MaterialTheme.typography.h2,
)
}
}
@Composable
public fun ScreenExample2() {
ConstraintLayout(
ConstraintSet {
val button = createRefFor("button")
val title = createRefFor("title")
val g1 = createGuidelineFromStart(80.dp)
constrain(button) {
top.linkTo(title.bottom, 16.dp)
start.linkTo(g1)
}
constrain(title) {
centerVerticallyTo(parent)
start.linkTo(g1)
}
},
modifier = Modifier
.fillMaxSize()
) {
Button(
modifier = Modifier.layoutId("button"),
onClick = {},
) {
Text(text = stringResource(id = R.string.log_in))
}
Text(modifier = Modifier.layoutId("title"),
text = stringResource(id = R.string.welcome_header),
style = MaterialTheme.typography.h2,
)
}
}
@Composable
public fun ScreenExample3() {
ConstraintLayout(
ConstraintSet("""
{
Header: { exportAs: 'example 3'},
g1: { type: 'vGuideline', start: 80 },
button: {
top: ['title', 'bottom', 16],
start: ['g1', 'start']
},
title: {
centerVertically: 'parent',
start: ['g1', 'start']
}
}
"""),
modifier = Modifier.fillMaxSize()
) {
Button(
modifier = Modifier.layoutId("button"),
onClick = {},
) {
Text(text = stringResource(id = R.string.log_in))
}
Text(modifier = Modifier.layoutId("title"),
text = stringResource(id = R.string.welcome_header),
style = MaterialTheme.typography.h2,
)
}
}
A ConstraintSet in ConstraintLayout can be used to specify a layout that contains the position rules of the layout, including the Constraints. A sample usage of ConstraintSet (in DSL and JSON5) can be seen in Example 1. The ConstraintSet creates a layout that aligns a Text with a Button horizontally and positions them in the center of the parent vertically.
DSL
// create a constraintSet variable to be passed to ConstraintLayout
val constraintSet = ConstraintSet {
// create a reference for the Text and Button based on the Id
val titleText = createRefFor("title")
val button = createRefFor("btn")
// Specify constrains of Text (titleText) and Button (button)
constrain(titleText) {
// add top constrain to parent top
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
// add end constrain to button start
end.linkTo(button.start)
}
constrain(button) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(titleText.end)
end.linkTo(parent.end)
}
}
// Pass the constraintSet variable to ConstraintLayout and create widgets
ConstraintLayout(constraintSet, modifier = Modifier.fillMaxSize()) {
Button(onClick = {}, modifier = Modifier.layoutId("btn")) {
Text(text = "button")
}
Text(text = "Hello World", modifier = Modifier.layoutId("title"))
}
JSON5
// create a constraintSet variable to be passed to ConstraintLayout
val constraintSet = ConstraintSet("""
{
title: {
top: ['parent', 'top'],
bottom: ['parent', 'bottom'],
start: ['parent', 'start'],
end: ['btn', 'start']
},
btn: {
top: ['parent', 'top'],
bottom: ['parent', 'bottom'],
start: ['title', 'end'],
end: ['parent', 'end'],
}
}
""".trimIndent())
// Pass the constraintSet variable to ConstraintLayout and create widgets
ConstraintLayout(constraintSet, modifier = Modifier.fillMaxSize()) {
Button(onClick = {}, modifier = Modifier.layoutId("btn")) {
Text(text = "button")
}
Text(text = "Hello World", modifier = Modifier.layoutId("title"))
}
Helpers such as Flow, Barrier, Grid (Row & Column) can also be used to position Composables within ConstraintLayout. Constraints combined with other parameters such as width, height, vBias and hBias control the layout of the Composebles.
Sample usages of the ConstraintLayout Helpers can be found in the table below:
DSL | JSON5 | |
---|---|---|
Barrier | Barriers.kt | test.kt |
Chain | Chains.kt | test.kt |
Flow | FlowDslDemo.kt | FlowDemo.kt |
Guideline | Guidelines.kt | test.kt |
Grid | GridDslDemo.kt | GridDemo.kt |
For more details about MotionLayout in Compose, please see
- Introduction to MotionLayout in Compose
- Compose MotionLayout JSON5 Syntax
- Compose MotionLayout DSL Syntax
The MotionScene of the MotionLayout is used to create an animation (or a transition). A MotionScene contains the ConstraintSets that represent different layouts and the transition between those ConstraintSets.
See Example 2 below for an example about how to create a basic animation with MotionScene. In the starting of the transition, a Text is constrained to the top and the start of its parent (see the first ConstraintSet). In the end of the transition, the Text will be constrained to the bottom and the end of its parent (see the second ConstraintSet).
DSL
// Create a MotionScene to be passed to MotionLayout
// A basic MotionScene contains two ConstrainSets (can be more) to indicate the starting layout
// and the ending layout as well as a Transition between the ConstrainSets
val motionScene = MotionScene {
val titleText = createRefFor("title")
// basic "default" transition
defaultTransition(
// specify the starting layout
from = constraintSet { // this: ConstraintSetScope
constrain(titleText) { // this: ConstrainScope
top.linkTo(parent.top)
start.linkTo(parent.start)
}
},
// specify the ending layout
to = constraintSet { // this: ConstraintSetScope
constrain(titleText) { // this: ConstrainScope
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
}
}
)
}
// Pass the MotionScence variable to MotionLayout and create Text
MotionLayout(motionScene,
progress = 0f, // progress when the transition starts (can be 0f - 1f)
modifier = Modifier.fillMaxSize()) {
Text(text = "Hello World", modifier = Modifier.layoutId("title"))
}
JSON5
// Create a MotionScene to be passed to MotionLayout
val motionScene = MotionScene("""
{
ConstraintSets: {
startLayout: {
title: {
top: ['parent', 'top'],
start: ['parent', 'start']
}
},
endLayout: {
title: {
bottom: ['parent', 'bottom'],
end: ['parent', 'end']
}
}
},
Transitions: {
default: {
from: 'startLayout',
to: 'endLayout'
}
}
}
""".trimIndent())
// Pass the MotionScence variable to MotionLayout and create Text
MotionLayout(motionScene,
progress = 0f, // progress when the transition starts (can be 0f - 1f)
modifier = Modifier.fillMaxSize()) {
Text(text = "Hello World", modifier = Modifier.layoutId("title"))
}
Adding the onSwipe behavior allows you to control gestures with swiping. Sample codes in Example 3 are for the onSwipe usage - onSwipe needs to be specified in a Transition. In the example, the onSwipe section sets an anchor on the start side of the Text. The direction of swipe is set to end, indicating the direction of the motion we are tracking.
DSL
defaultTransition(
// specify the starting layout
from = startConstraintSet,
// specify the ending layout
to = endConstraintSet,
) {
onSwipe = OnSwipe(
anchor = titleText,
direction = SwipeDirection.End,
side = SwipeSide.Start,
)
}
JSON5
Transitions: {
default: {
from: 'startLayout',
to: 'endLayout',
onSwipe: {
anchor: 'title',
direction: 'end',
side: 'start',
}
}
- anchor - the Composable you wish your finger to track
- side - the side of the anchor Composable your finger will track
- direction - direction of you motion
- scale - a scale factor to magnify your motion
- maxVelocity - the maximum speed you can make progress at (progress per second)
- maxAccel - the maximum the system can accelerate progress changes (progress per second per second)
- mode - spring or velocity mode.
- touchUp - what happens on lifting your finger
- springMass - the mass of the Spring in spring mode
- springStiffness - the stiffness of the spring in spring mode
- springDamping - the damping of the spring spring mode
- stopThreshold - the speed threshold below which the animation is ended in spring mode
- springBoundary - what happens when the spring hits the target (0 or 1) overshoot or bounce
KeyFrames in MotionLayout was created to help create more sophisticated transitions. We currently support 3 types of KeyFrames, including KeyAttributes, KeyPositions and KeyCycles. They can be set and/or updated during the progress of a transition (0f - 1f).
Example 4 contains the sample codes for how to use Keyframes (KeyAttributes and KeyPositions) in a Transition. In this code, the transition of the alpha value (for titleText) is specified with KeyAttributes and the x position with KeyPositions at the 20% and 80% progress.
DSL
defaultTransition(
// specify the starting layout
from = startConstraintSet,
// specify the ending layout
to = endConstraintSet,
) {
val titleText = createRefFor("title")
// change the alpha value at 20% and 80% of the progress
keyAttributes(titleText) {
frame(20) {
alpha = 0.2f
}
frame(80) {
alpha = 0.8f
}
}
// change the x position at 20% and 80% of the progress
keyPositions(titleText) {
frame(20) {
percentX = 0.6f
}
frame(80) {
percentX = 0.3f
}
}
}
JSON5
Transitions: {
default: {
// specify the starting layout
from: 'startLayout',
// specify the ending layout
to: 'endLayout',
// specify the Keyframes
KeyFrames: {
// specify the KeyPositions
// change x position between 20% - 80% of the progress
KeyPositions: [
{
target: ['title'],
frames: [20, 80],
percentX: [0.6, 0.3],
}
],
// specify the KeyAttributes:
// change alpha value between 20% - 80% of the progress
KeyAttributes: [
{
target: ['title'],
frames: [20, 80],
alpha: [0.2, 0.8],
}
]
}
}
}
The relevant Transition Attributes can found below:
- percentWidth
- percentHeight
- sizePercent
- percentX
- percentY
- frame - the widgets to transform
- target - the key frames
- curveFit - interpolate between points using linear interpolation or a monotonic spline
- alpha - how transparent the widget should be.
- rotationZ - post layout rotate the widget
- rotationX - post layout rotate about a horizontal axis
- rotationY - post layout rotate about a vertical axis
- pivotX - post layout the point around which to rotate
- pivotY - post layout the point around which to rotate
- pathRotate - The angle with respect to the path of the widget to rotate
- scaleX - post layout
- scaleY - post layout
- translationX - post layout move the widget to the right
- translationY - post layout move widget down
- translationZ - post layout move widget towards the viewer expanding its shadow
(Not implemented yet pivotTarget, easing, progress)
- offset - the center of the wave
- phase - the phase angle of the wave in this area
- period - the number of waves to generate in this area
- frame - The widgets to transform
- target - the key frames
- curveFit - interpolate between points using linear interpolation or a monotonic spline
- alpha - how transparent the widget should be.
- rotationZ - post layout rotate the widget
- rotationX - post layout rotate about a horizontal axis
- rotationY - post layout rotate about a vertical axis
- pivotX - post layout the point around which to rotate
- pivotY - post layout the point around which to rotate
- pathRotate - The angle with respect to the path of the widget to rotate
- scaleX - post layout
- scaleY - post layout
- translationX - post layout move the widget to the right
- translationY - post layout move widget down
- translationZ - post layout move widget towards the viewer expanding its shadow
(Not implemented yet pivotTarget, easing, progress ,waveShape )