Designing for foldables

In the ConstraintLayout 2.1 release, several features were added to help manage foldable devices, including SharedValues, ReactiveGuide, and enhanced support for animation with MotionLayout.

Shared Values

We added a new mechanism to inject runtime values in ConstraintLayout – this is intended to be used for system-wide values, as all instances of ConstraintLayout are able to access the value.

In the context of foldable devices, we can use this mechanism to inject the position of the fold at runtime:

Kotlin

ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)

Java

ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold);

In a custom helper, you can access the shared values by adding a listener for any changes:

Kotlin

val sharedValues: SharedValues = ConstraintLayout.getSharedValues()
sharedValues.addListener(mAttributeId, this)

Java

SharedValues sharedValues = ConstraintLayout.getSharedValues();
sharedValues.addListener(mAttributeId, this);

You can look at the FoldableExperiments example to see how we capture the position of the fold using the Jetpack WindowManager library and inject the position into ConstraintLayout.

Kotlin

inner class StateContainer : Consumer<WindowLayoutInfo> {

    override fun accept(newLayoutInfo: WindowLayoutInfo) {

        // Add views that represent display features
        for (displayFeature in newLayoutInfo.displayFeatures) {
            val foldFeature = displayFeature as? FoldingFeature
            if (foldFeature != null) {
                if (foldFeature.isSeparating &&
                    foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
                ) {
                    // The foldable device is in tabletop mode
                    val fold = foldPosition(motionLayout, foldFeature)
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)
                } else {
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0);
                }
            }
        }
    }
}

Java

class StateContainer implements Consumer<WindowLayoutInfo> {

    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {

        // Add views that represent display features
        for (DisplayFeature displayFeature : newLayoutInfo.getDisplayFeatures()) {
            if (displayFeature instanceof FoldingFeature) {
                FoldingFeature foldFeature = (FoldingFeature)displayFeature;
                if (foldFeature.isSeparating() &&
                    foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL
                ) {
                    // The foldable device is in tabletop mode
                    int fold = foldPosition(motionLayout, foldFeature);
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold);
                } else {
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0);
                }
            }
        }
    }
}

fireNewValue() takes an ID representing the value as the first parameter and the value to inject as the second parameter.

ReactiveGuide

One way to take advantage of a SharedValue in a layout, without having to write any code, is to use the ReactiveGuide helper. This will position a horizontal or vertical guideline according to the linked SharedValue.

    <androidx.constraintlayout.widget.ReactiveGuide
        android:id="@+id/fold"
        app:reactiveGuide_valueId="@id/fold"
        android:orientation="horizontal" />

It can then be used as a you would with a normal guideline.

MotionLayout for foldables

We added several features in MotionLayout in 2.1 that helps morphing state – something particularly useful for foldables, as we typically have to handle animating between the different possible layouts.

There are two approaches available for foldables:

  • At runtime, update your current layout (ConstraintSet) to show or hide the fold.
  • Use a separate ConstraintSet for each of the foldable states you want to support (closed, folded, or fully open).

Animating a ConstraintSet

The function updateStateAnimate() in MotionLayout was added in the 2.1 release:

Kotlin

fun updateStateAnimate(stateId: Int, set: ConstraintSet, duration: Int)

Java

void updateStateAnimate(int stateId, ConstraintSet set, int duration);

This function will automatically animate the changes when updating a given ConstraintSet instead of doing an immediate update (which you can do with updateState(stateId, constraintset)). This allows you to update your UI on the fly, depending on changes, such as which foldable state you are in.

ReactiveGuide inside a MotionLayout

ReactiveGuide also supports two useful attributes when used inside a MotionLayout:

  • app:reactiveGuide_animateChange="true|false"

  • app:reactiveGuide_applyToAllConstraintSets="true|false"

The first one will modify the current ConstraintSet and animate the change automatically. The second one will apply the new value of the ReactiveGuide position to all ConstraintSets in the MotionLayout. A typical approach for foldables would be to use a ReactiveGuide representing the fold position, setting up your layout elements relative to the ReactiveGuide.

Using multiple ConstraintSets to represent foldable state

Instead of updating the current MotionLayout state, another way to architect your UI to support foldables is to create specific separate states (including closed, folded, and fully open).

In this scenario, you might still want to use a ReactiveGuide to represent the fold, but you would have a lot more control (compared to the automated animation when updating the current ConstraintSet) on how each state would transition into another.

With this approach, in your DeviceState listener, you would simply direct the MotionLayout to transition to specific states through the MotionLayout.transitionToState(stateId) method.