Tabletop Mode with existing layouts
Adding foldable support to MotionLayout and ConstraintLayout
One of the innovations in Android foldables is Tabletop Mode (or “Flex Mode” on Samsung), where a device with a horizontal fold or hinge is partially folded. The fold creates a natural boundary to separate views. For example, a video plays on the top portion with its controls on the bottom.
The good news is that you do not need to completely rewrite your app to support TableTop Mode. Constraint Layout 2.1 added some tools to help modify existing layouts to deal with foldables. In combination with Jetpack Window Manager, a ReactiveGuide can move its position at runtime when Tabletop Mode is detected and show the bottom layout.
With MotionLayout
If you are using MotionLayout
, add a horizontal ReactiveGuide
that is constrained to both the top and bottom portions. Set the guide’s layout_constraintGuide_end
to 0dp
so that it is anchored to the bottom of the screen. This will be the default configuration where the top fills the entire screen while the device is open.
<androidx.constraintlayout.motion.widget.MotionLayout>
<TextView
android:text="Top"
app:layout_constraintBottom_toTopOf="@+id/fold"
... />
<androidx.constraintlayout.widget.ReactiveGuide
app:reactiveGuide_valueId="@id/fold"
android:orientation="horizontal"
app:layout_constraintGuide_end="0dp"
... />
<View
app:layout_constraintTop_toTopOf="@+id/fold"
android:background="@color/teal_200"
... />
</><androidx.constraintlayout.motion.widget.MotionLayout>
<TextView
android:text="Top"
app:layout_constraintBottom_toTopOf="@+id/fold"
... />
<androidx.constraintlayout.widget.ReactiveGuide
app:reactiveGuide_valueId="@id/fold"
android:orientation="horizontal"
app:layout_constraintGuide_end="0dp"
... />
<View
app:layout_constraintTop_toTopOf="@+id/fold"
android:background="@color/teal_200"
... />
</>
When the device is folded 90°, use Jetpack Window Manager to measure the fold position without a configuration change. Update the position by firing a new shared value when the device is half open. Make sure that the position is relative to the bottom of the screen since the ReactiveGuide
is using layout_constraintGuide_end
and not layout_constraintGuide_begin
.
ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, newFoldPosition)
This update to R.id.fold
animates the ReactiveGuide
by updating its ConstraintSet to the new fold position via the MotionLayout
.
/* ReactiveGuide.java */
ConstraintSet constraintSet = motionLayout.cloneConstraintSet(currentState);
constraintSet.setGuidelineEnd(id, newValue);
motionLayout.updateStateAnimate(currentState, constraintSet, 1000);
When the device is fully opened again, reset the position to 0
so that bottom view slides down causing the top to fill up the entire layout again.
ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0)
ReactiveGuide inside a ConstraintLayout
It’s not necessary to use MotionLayout
to support TableTop Mode. You can still achieve a similar effect without the animation if you’re only using ConstraintLayout
.
Setup is nearly identical to MotionLayout
. The one key difference is the offset of the ReactiveGuide
needs to change relative to the start of layout rather than the end. ReactiveGuide
first sets the new value using setGuidelineBegin() and then the rest of the function related to MotionLayout
and the end guideline is ignored.
/* ReactiveGuide.java */
public void onNewValue(int key, int newValue, int oldValue) {
setGuidelineBegin(newValue);
if (getParent() instanceof MotionLayout) {
/* Ignored when using ConstaintLayout */
...
}
}
Finally, you again need to update the default position in order for the top view to fill the screen when the device is fully open. However, since the offset is from the beginning instead of the end like in MotionLayout
, the default position of the ReactiveGuide
becomes the full height of the layout rather than 0
.
ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, layout.height)