Skip to content

Quick Guide to ConstraintLayout and MotionLayout in Compose

Shane Wong edited this page Mar 10, 2023 · 2 revisions

Intro to ConstraintLayout 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:

  1. Introduction to ConstraintLayout in Compose
  2. ConstraintSet JSON5 syntax
  3. Compose ConstraintLayout DSL Syntax

Constraints

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)

3 ways of setting Constraints

Modifiers

Full Example

@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,
       )
   }
}

ConstraintSet DSL

Full Example

@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,
       )
   }
}

ConstraintSet JSON5

Full Example

@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,
       )
   }
}

ConstraintSets

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.

Example 1

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

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

Intro to MotionLayout in Compose

For more details about MotionLayout in Compose, please see

  1. Introduction to MotionLayout in Compose
  2. Compose MotionLayout JSON5 Syntax
  3. Compose MotionLayout DSL Syntax

MotionScene

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).

Example 2

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"))
    }

OnSwipe

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.

Example3

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',
              }
           }

OnSwipe Attributes

  • 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

MotionLayout Transitions

KeyFrames

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.

Example 4

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],
                    }
                  ]
                }
              }
           }

Transition Attributes

The relevant Transition Attributes can found below:

KeyPositions

  • percentWidth
  • percentHeight
  • sizePercent
  • percentX
  • percentY

KeyAttributes

  • 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)

KeyCycles

  • 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 )