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
, orfully 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 ConstraintSet
s 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 ConstraintSet
s 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.