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 View
s 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 Flow
s. 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"
The next step is make sure your app receives information from Jetpack WindowManager. First, let's declare an instance variable:
private WindowInfoTrackerCallbackAdapter adapter;
It is initialized in onCreate()
:
adapter = new WindowInfoTrackerCallbackAdapter(
WindowInfoTracker.Companion.getOrCreate(
this
)
);
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);
…
}
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();
…
});
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 Fragment
s, 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;
}
}
}
…
So, what is happening here? If we find a hinge, we
- set the sizes of
leftPane
,rightPane
, andhinge
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;
}
}
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.
Top comments (0)