Tabletop Mode with existing layouts

Adding foldable support to MotionLayout and ConstraintLayout

Bryan Parker
3 min readMar 29, 2023
Photo by Mika Baumeister on Unsplash

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.

Google Duo on the Samsung Galaxy Z Flip

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

TableTop mode 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.

TableTop mode with 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)

--

--

Bryan Parker
0 Followers

Android Developer @ The Walt Disney Company