Large unfolded displays and unique folded states enable new user experiences on foldable devices. To make your app fold aware, use the Jetpack WindowManager library, which provides an API surface for foldable device window features such as folds and hinges. When your app is fold aware, it can adapt its layout to avoid placing important content in the area of folds or hinges and use folds and hinges as natural separators.
Window information
The WindowInfoTracker
interface in Jetpack WindowManager exposes window layout information. The interface's windowLayoutInfo()
method returns a stream of WindowLayoutInfo
data that informs your app about a foldable device’s fold state. The WindowInfoTracker
getOrCreate()
method creates an instance of WindowInfoTracker
.
WindowManager provides support for collecting WindowLayoutInfo
data using Kotlin Flows and Java callbacks.
Kotlin Flows
To start and stop WindowLayoutInfo
data collection, you can use a restartable lifecycle-aware coroutine in which the repeatOnLifecycle
code block is executed when the lifecycle is at least STARTED
and stopped when the lifecycle is STOPPED
. Execution of the code block is automatically restarted when the lifecycle is STARTED
again. In the following example, the code block collects and uses WindowLayoutInfo
data:
class DisplayFeaturesActivity : AppCompatActivity() {
private lateinit var binding: ActivityDisplayFeaturesBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
Java callbacks
The callback compatibility layer included in the androidx.window:window-java
dependency enables you to collect WindowLayoutInfo
updates without using a Kotlin Flow. The artifact includes the WindowInfoTrackerCallbackAdapter
class, which adapts a WindowInfoTracker
to support registering (and unregistering) callbacks to receive WindowLayoutInfo
updates, for example:
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
RxJava support
If you're already using RxJava
(version 2
or 3
), you can take advantage of artifacts that enable you to use an Observable
or Flowable
to collect WindowLayoutInfo
updates without using a Kotlin Flow.
The compatibility layer provided by the androidx.window:window-rxjava2
and androidx.window:window-rxjava3
dependencies includes the WindowInfoTracker#windowLayoutInfoFlowable()
and WindowInfoTracker#windowLayoutInfoObservable()
methods, which enable your app to receive WindowLayoutInfo
updates, for example:
class RxActivity: AppCompatActivity {
private lateinit var binding: ActivityRxBinding
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Create a new observable
observable = WindowInfoTracker.getOrCreate(this@RxActivity)
.windowLayoutInfoObservable(this@RxActivity)
}
@Override
protected void onStart() {
super.onStart();
// Subscribe to receive WindowLayoutInfo updates
disposable?.dispose()
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newLayoutInfo ->
// Use newLayoutInfo to update the layout
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose the WindowLayoutInfo observable
disposable?.dispose()
}
}
Features of foldable displays
The WindowLayoutInfo
class of Jetpack WindowManager makes the features of a display window available as a list of DisplayFeature
elements.
A FoldingFeature
is a type of DisplayFeature
that provides information about foldable displays, including the following:
state
: The folded state of the device,FLAT
orHALF_OPENED
orientation
: The orientation of the fold or hinge,HORIZONTAL
orVERTICAL
occlusionType
: Whether the fold or hinge conceals part of the display,NONE
orFULL
isSeparating
: Whether the fold or hinge creates two logical display areas, true or false
A foldable device that is HALF_OPENED
always reports isSeparating
as true because the screen is separated into two display areas. Also, isSeparating
is always true on a dual-screen device when the application spans both screens.
The FoldingFeature
bounds
property (inherited from DisplayFeature
) represents the bounding rectangle of a folding feature such as a fold or hinge. The bounds can be used to position elements on screen relative to the feature.
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // Safely collects from windowInfoRepo when the lifecycle is STARTED // and stops collection when the lifecycle is STOPPED WindowInfoTracker.getOrCreate(this@MainActivity) .windowLayoutInfo(this@MainActivity) .collect { layoutInfo -> // New posture information val foldingFeature = layoutInfo.displayFeatures .filterIsInstance() .firstOrNull() // Use information from the foldingFeature object } } } }
Java
private WindowInfoTrackerCallbackAdapter windowInfoTracker; private final LayoutStateChangeCallback layoutStateChangeCallback = new LayoutStateChangeCallback(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ... windowInfoTracker = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this)); } @Override protected void onStart() { super.onStart(); windowInfoTracker.addWindowLayoutInfoListener( this, Runnable::run, layoutStateChangeCallback); } @Override protected void onStop() { super.onStop(); windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback); } class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> { @Override public void accept(WindowLayoutInfo newLayoutInfo) { // Use newLayoutInfo to update the Layout List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures(); for (DisplayFeature feature : displayFeatures) { if (feature instanceof FoldingFeature) { // Use information from the feature object } } } }
Tabletop mode
Using the information included in the FoldingFeature
object, your app can support postures like tabletop mode,
where the phone sits on a surface, the hinge is in a horizontal position, and the foldable screen is half opened.
Tabletop mode offers users the convenience of operating their phones without holding the phone in their hands. Tabletop mode is great for watching media, taking photos, and making video calls.
Use FoldingFeature.State
and FoldingFeature.Orientation
to determine whether the device is in tabletop mode:
Kotlin
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL }
Java
boolean isTableTopPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL); }
Once you know the device is in tabletop mode, update your app layout accordingly. For media apps, that typically means placing the playback above the fold and positioning controls and supplementary content just below for a hands-free viewing or listening experience.
Examples
MediaPlayerActivity
app: See how to use Media3 Exoplayer and WindowManager to create a fold-aware video player.Unfold your camera experience codelab: Learn how to implement tabletop mode for photography apps. Show the viewfinder on the top half of the screen, above the fold, and the controls on the bottom half, below the fold.
Book mode
Another unique foldable posture is book mode, where the device is half opened and the hinge is vertical. Book mode is great for reading e-books. With a two-page layout on a large screen foldable open like a bound book, book mode captures the experience of reading a real book.
It can also be used for photography if you want to capture a different aspect ratio while taking pictures hands-free.
Implement book mode with the same techniques used for tabletop mode. The only difference is the code should check that the folding feature orientation is vertical instead of horizontal:
Kotlin
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.VERTICAL }
Java
boolean isBookPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL); }
Window size changes
An app's display area can change as a result of a device configuration change—for example, when the device is folded or unfolded, rotated, or a window is resized in multi-window mode.
The Jetpack WindowManager WindowMetricsCalculator
class enables you to retrieve the current and maximum window metrics. Like the platform WindowMetrics
introduced in API level 30, the WindowManager WindowMetrics
provide the window bounds, but the API is backward compatible down to API level 14.
See Window size classes.
Additional resources
Samples
- Jetpack WindowManager: Example of how to use the Jetpack WindowManager library
- Jetcaster: Tabletop posture implementation with Compose
Codelabs
Recommended for you
- Note: link text is displayed when JavaScript is off
- Support foldable and dual-screen devices with Jetpack WindowManager
- Optimize your camera app on foldable devices with Jetpack WindowManager
- Device compatibility mode