blob: 9fbed8ba198dcb2a683daadc454c2588eb655397 [file] [log] [blame] [view]
## Android WebView & Insets
## Context
WebView is an Android component which you can use to display web content in your
app. Just like a web browser, WebView contains multiple
[viewports](https://developer.mozilla.org/en-US/docs/Glossary/Viewport) and your
content’s alignment may change based on the size of different viewports.
The
[Layout Viewport](https://developer.mozilla.org/en-US/docs/Glossary/Layout_viewport)
usually remains fixed in place and represents the size of the page. Elements
with `position: fixed` are attached to the layout viewport. Conversely, the
[Visual Viewport](https://developer.mozilla.org/en-US/docs/Glossary/Visual_Viewport)
represents the screen’s visible slice of the page. If the user zooms, pans or
scrolls, the Visual Viewport may show a different portion of the page.
## What Changed?
**In M136**, WebView added support for
[displayCutout()](https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type#displayCutout())
insets and
[systemBars()](https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type#systemBars())
insets. These were forwarded to the web content via the safe-area-insets CSS
values. Due to compatibility issues, we only supported passing these inset
values to the web content when the WebView was fullscreen.
**Pre-M139**, both WebView’s Layout and Visual Viewports were fixed and
identical. This caused some issues on newer devices as we were unable to
communicate to the web developer which parts of the screen were obscured by the
keyboard. **In M139**, WebView adopted support for the
[ime()](https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type#ime())
inset type so that the Visual Viewport could be resized independently of the
Layout Viewport. In the same milestone, WebView added logic to resize the visual
viewport based on which portion of the WebView is visible to the user. For
example, if the WebView has a height of 3000px but only the top 1000px is
visible on screen, the WebView's visual viewport will be resized to fit the
portion that is visible. This only applies to when the bottom of a WebView moves
off screen; the same is not true if the top, left or right of the WebView moves
past the edge of the screen. At the time of writing, the current level of
support is as follows:
WebView respects the following inset types:
* displayCutout()
* systemBars()
* ime()
displayCutout and systemBars are forwarded to the web content via the
[safe-area-insets](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Environment_variables/Using#safe-area-inset-)
API. The ime insets directly affect the size of the Visual Viewport.
**Pre-M139**, the IME would show and sometimes it was impossible to view some
elements which had become hidden. With the IME insets modifying the Visual
Viewport, the visible portion of the page will by default be scrollable so the
user can view the hidden content.
Now that keyboard visibility changes the Visual Viewport, you may see a greater
number of resize events fired in your web code. You should revisit your code to
ensure that it’s not doing something unexpected when the page resizes. We’ve
seen a number of reports where the resize events are clearing the page’s focus.
This makes focusing an input element impossible as the following occurs:
User focuses input -> Keyboard shows -> Resize event is fired -> Page focus is
cleared -> Keyboard is hidden.
**In M144**, we expanded inset support to all WebViews regardless of whether
they are displaying fullscreen or not. The insets should still only report
non-zero values when the UI directly overlaps with WebView’s bounds. For
example, if you have a WebView in the middle of the screen where no part of it
overlaps with the system bars, display cutout or ime, it is the expected
behavior that all the inset values report 0 to the web content. However, when
there is any overlap between the WebView and the display cutout, system bars or
ime, the web content will receive the correct padding values through the
corresponding channel (safe-area-insets or visual viewport resizing).
## What is needed from apps?
Please check your app’s code to ensure that it is handling WindowInsets
correctly. Insets can be handled either by an
[OnApplyWindowInsetsListener](https://developer.android.com/reference/android/view/View.OnApplyWindowInsetsListener)
or via the
[onApplyWindowInsets](https://developer.android.com/reference/android/view/View#onApplyWindowInsets(android.view.WindowInsets))
method; both have almost the same signature (the former has an additional view
parameter).
```
ViewCompat.setOnApplyWindowInsetsListener(webView, (view, insets) -> {
// Process insets here and return those not consumed by this method.
});
// Alternatively...
class MyWebView extends WebView {
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
// Process insets here and return those not consumed by this method.
}
}
```
The WindowInsets are passed to the method where your UI can process them. For
any insets where your app adds accommodating padding, the WindowInsets returned
from your handler should either have those inset types set to zero (preferred)
or consumed (see below). If you return the constant
[WindowInsets.CONSUMED](https://developer.android.com/reference/android/view/WindowInsets#CONSUMED)
from your method, WebView will not send any safe area insets to the web
contents. If you return the insets passed to the method and also apply padding
to your activity, you may see extra spacing in the WebView.
The difference between marking insets as consumed and marking them as zero is
only relevant if the insets consumed by your handler have changed. For example,
if you previously did not add any padding to your activity and then, based on
some external state, started adding padding for one type of inset, you should
set the newly changed insets to zero rather than consuming them. This is because
inset handler calls are propagated down the view hierarchy but traversal stops
when insets are consumed. Therefore, if you marked the newly consumed insets as
CONSUMED instead of setting them to zero, WebView would continue to show the old
insets as it wouldn’t receive any notification to change its internal padding.
See below for an example of a valid insets listener:
```
ViewCompat.setOnApplyWindowInsetsListener(rootView, (view, windowInsets) -> {
int types = WindowInsetsCompat.Type.systemBars()
| WindowInsetsCompat.Type.displayCutout();
Insets insets = windowInsets.getInsets(types);
rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
// 0 the consumed insets.
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(types, Insets.NONE)
.build();
});
```
## Opt-Out
If you don’t want the new behavior in your app, you can easily opt out. Android
provides the setOnApplyWindowInsetsListener API with which you can intercept and
consume any insets that you don’t want WebView to handle. If you have subclassed
WebView, you can also override onApplyWindowInsets directly and consume the
insets there. Note that if you are consuming these insets, you should ensure
that your UI is properly configured to either resize or add padding so as not to
overlap whatever occlusion the insets are reporting. To force WebView to reset
its insets, do not pass consumed insets. Instead, clear the insets and pass them
to the WebView. This will clear the internal safe-area-insets and will return
the Visual Viewport to its initial size (the size of the Layout Viewport).
If you decide to opt out, some features may not work as expected. The keyboard
may cover inputs near the bottom of the page and in some cases, scrollIntoView
may not work properly.