Make your app fold aware

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 or HALF_OPENED
  • orientation: The orientation of the fold or hinge, HORIZONTAL or VERTICAL
  • occlusionType: Whether the fold or hinge conceals part of the display, NONE or FULL
  • 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.

A video player app in tabletop mode

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

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 how to support different window sizes in Support different screen sizes.

Additional resources

Samples

Codelabs