DEV Community

Cover image for Folding legacy apps
Thomas Künneth
Thomas Künneth

Posted on

Folding legacy apps

This series focuses on foldable and large-screen devices. Most of its articles use Kotlin, which has become the preferred programming language for Android apps quite a few years ago. Yet, there are still a lot of Android apps based on Java. Referring to them as legacy apps doesn't mean they no longer are under active development, or, at least in maintenance mode. Therefore, shouldn't we make sure that they provide a great experience on new device categories, too? We certainly should. So, in this article I show you how to make a Java Android app using Views behave well on foldables and large screens.

Jetpack WindowManager

With Jetpack WindowManager, you can query certain device characteristics related to foldables. For example, the library tells you if a device has a hinge and if that hinge blocks certain areas of the screen. This, for example, is the case for the dual screen device Surface Duo and its successor.

Jetpack WindowManager uses a Kotlin-friendly api based on Flows. Fortunately, it gives Java some love, too. To use the library in your Java app, you need to add an implementation dependency in your module-level build.gradle file:

implementation "androidx.window:window-java:1.0.0-rc01"
Enter fullscreen mode Exit fullscreen mode

The next step is make sure your app receives information from Jetpack WindowManager. First, let's declare an instance variable:

private WindowInfoTrackerCallbackAdapter adapter;
Enter fullscreen mode Exit fullscreen mode

It is initialized in onCreate():

adapter = new WindowInfoTrackerCallbackAdapter(
    WindowInfoTracker.Companion.getOrCreate(
        this
    )
);
Enter fullscreen mode Exit fullscreen mode

Now it is time to wire it up with our code. For reasons of brevity I am using old-style Activity callbacks, but please consider using Jetpack Lifecycle instead.

@Override
protected void onStart() {
  super.onStart();
  adapter.addWindowLayoutInfoListener(this,
      ContextCompat.getMainExecutor(this),
      callback);
  
}

@Override
protected void onStop() {
  super.onStop();
  adapter.removeWindowLayoutInfoListener(callback);
  
}
Enter fullscreen mode Exit fullscreen mode

So, what is callback?

private final Consumer<WindowLayoutInfo> callback =
    (windowLayoutInfo -> {
  final var windowMetrics = WindowMetricsCalculator.getOrCreate()
      .computeCurrentWindowMetrics(this);
  final var windowWidth = windowMetrics.getBounds().width();
  final var windowHeight = windowMetrics.getBounds().height();
  final var leftPaneParams = binding.leftPane.getLayoutParams();
  final var rightPaneParams = binding.rightPane.getLayoutParams();
  final var hingeParams = binding.hinge.getLayoutParams();
  var hasFoldingFeature = false;
  List<DisplayFeature> displayFeatures
        = windowLayoutInfo.getDisplayFeatures();
  
});
Enter fullscreen mode Exit fullscreen mode

First, we setup a couple of variables. WindowMetricsCalculator helps us obtain window metrics. So, windowWidth and windowHeight contain the width and the height. What about leftPane, rightPane, and hinge?

Layout structure

leftPane and rightPane represent the content of your app, presented as two columns. hinge is placed between them. All three are children of a LinearLayout, whose orientation will change if needed. I'll show you soon.

Now, you may be thinking But my app doesn't use such a structure.

I know. Most legacy apps are not optimized for tablets. But that is the beauty of this simple approach. Just make the root of your user interface leftPane, add hinge and rightPane, and wrap all three in a LinearLayout. If you app heavily relies on Fragments, a more substantial rewowrk may be due.

Now, let's look at how to process displayFeatures.

for (DisplayFeature displayFeature : displayFeatures) {
    FoldingFeature foldingFeature =
        (FoldingFeature) displayFeature;
    if (foldingFeature != null) {
      hasFoldingFeature = true;
      boolean isVertical = foldingFeature.getOrientation()
          == FoldingFeature.Orientation.VERTICAL;
      final var foldingFeatureBounds = foldingFeature.getBounds();
      hingeParams.width = foldingFeatureBounds.width();
      hingeParams.height = foldingFeatureBounds.height();
      if (isVertical) {
        binding.parent.setOrientation(LinearLayout.HORIZONTAL);
        leftPaneParams.width = foldingFeatureBounds.left;
        leftPaneParams.height =
            LinearLayout.LayoutParams.MATCH_PARENT;
        rightPaneParams.width = windowWidth
                                - foldingFeatureBounds.right;
        rightPaneParams.height =
           LinearLayout.LayoutParams.MATCH_PARENT;
      } else {
        int[] intArray = new int[2];
        binding.leftPane.getLocationOnScreen(intArray);
        binding.parent.setOrientation(LinearLayout.VERTICAL);
        leftPaneParams.width =
            LinearLayout.LayoutParams.MATCH_PARENT;
        leftPaneParams.height =
            foldingFeatureBounds.top - intArray[1];
        rightPaneParams.width =
            LinearLayout.LayoutParams.MATCH_PARENT;
        rightPaneParams.height = windowHeight
                                 - foldingFeatureBounds.bottom;
      }
    }
  }
  
Enter fullscreen mode Exit fullscreen mode

So, what is happening here? If we find a hinge, we

  • set the sizes of leftPane, rightPane, and hinge accordingly
  • configure the orientation of the LieaerLayout based on the configuration of the hinge

Did you notice getLocationOnScreen()? This code is executed when the orientation of the hinge is horizontal. Then, leftPane and rightPane are arranged vertically, with hinge between them. Although both screens usually have an equal size, one screen will contain the statusbar and an appbar, so the area for the content will be smaller. I found that calculation the offset this way is most reliable.

Our last step is to deal with the situation that there is no hinge. This is the case for normal smartphones and tablets.

if (!hasFoldingFeature) {
    final float density =
       getResources().getDisplayMetrics().density;
    final float dp = windowMetrics.getBounds().width() / density;
    binding.parent.setOrientation(LinearLayout.HORIZONTAL);
    hingeParams.width = 0;
    hingeParams.height = 0;
    if (dp >= 600) {
      leftPaneParams.width = windowWidth / 2;
      leftPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
      rightPaneParams.width = windowWidth / 2;
      rightPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
    } else {
      leftPaneParams.width =
          LinearLayout.LayoutParams.MATCH_PARENT;
      leftPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
      rightPaneParams.width = 0;
      rightPaneParams.height = 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

I decide if I want to use two column mode by calculation the screen width in density independent pixels. If the computed value is less than 600 I configure a single column layout. Otherwise leftPane and rightPane will be of equal size. The size of the hinge is set to 0 in any case.

Conclusion

In this article I showed you how to incorporate Jetpack WindowManager into Java Android apps. Updating an existing layout to support a two column mode on foldables and large-screen devices is amazingly simple in many cases.

Have you made a legacy app look good on foldables? Please share your thoughts in the comments.

Discussion (0)