Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
55c4ce0
update example app
bparrishMines Mar 5, 2026
6bb5a99
implementation
bparrishMines Mar 6, 2026
8e10a01
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Mar 6, 2026
3b3d6c6
formatting
bparrishMines Mar 6, 2026
32856f9
java test
bparrishMines Mar 6, 2026
ce2a9b3
update unit tests
bparrishMines Mar 6, 2026
bc04474
add destruction back
bparrishMines Mar 6, 2026
57ca57c
version bump and undo example app
bparrishMines Mar 6, 2026
49a3867
formatting
bparrishMines Mar 6, 2026
74bfe0f
docs
bparrishMines Mar 6, 2026
cfdf061
fix docs
bparrishMines Mar 6, 2026
07c8699
update pigeon
bparrishMines Mar 6, 2026
31fa299
fix docs again
bparrishMines Mar 6, 2026
a8a415b
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Mar 13, 2026
b367d63
improve code logic
bparrishMines Mar 13, 2026
77b40da
other windowinsets
bparrishMines Mar 16, 2026
1bb6433
fix tests
bparrishMines Mar 16, 2026
4d02edb
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Mar 16, 2026
bb20265
add remove back
bparrishMines Mar 16, 2026
f42bb7b
fix main.dart
bparrishMines Mar 16, 2026
651a4f8
remove padding comment
bparrishMines Mar 16, 2026
204e405
change name of enum
bparrishMines Mar 18, 2026
681eb73
fix unit tests
bparrishMines Mar 18, 2026
6a6b394
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Mar 25, 2026
c93e0e6
add optional method
bparrishMines Mar 27, 2026
4e1915d
undo main changes
bparrishMines Mar 27, 2026
819f6d1
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Mar 27, 2026
bdf08ae
fix space
bparrishMines Mar 27, 2026
5c89d64
Merge branch 'main' of github.com:flutter/packages into webview_safe_…
bparrishMines Apr 3, 2026
c1511d2
test all values of androidwebviewinsets separately
bparrishMines Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/webview_flutter/webview_flutter_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.10.14

* Adds opt out of `displayCutout` and `systemBars` Android inset changes. See
https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/insets.md#opt_out

## 4.10.13

* Bumps androidx.webkit:webkit from 1.14.0 to 1.15.0.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.4), do not edit directly.
// Autogenerated from Pigeon (v26.1.10), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down Expand Up @@ -708,6 +708,7 @@ private class AndroidWebkitLibraryPigeonProxyApiBaseCodec(
value is OverScrollMode ||
value is SslErrorType ||
value is MixedContentMode ||
value is WindowInsets ||
value == null) {
super.writeValue(stream, value)
return
Expand Down Expand Up @@ -1098,6 +1099,29 @@ enum class MixedContentMode(val raw: Int) {
}
}

/**
* Defines different types of sources causing window insets.
*
* See https://developer.android.com/reference/kotlin/android/view/WindowInsets.Type
*/
enum class WindowInsets(val raw: Int) {
/**
* All system bars.
*
* Includes statusBars(), captionBar() as well as navigationBars(), systemOverlays(), but not
* ime().
*/
SYSTEM_BARS(0),
/** An inset type representing the area that used by DisplayCutout. */
DISPLAY_CUTOUT(1);

companion object {
fun ofRaw(raw: Int): WindowInsets? {
return values().firstOrNull { it.raw == raw }
}
}
}

private open class AndroidWebkitLibraryPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
Expand All @@ -1116,6 +1140,9 @@ private open class AndroidWebkitLibraryPigeonCodec : StandardMessageCodec() {
133.toByte() -> {
return (readValue(buffer) as Long?)?.let { MixedContentMode.ofRaw(it.toInt()) }
}
134.toByte() -> {
return (readValue(buffer) as Long?)?.let { WindowInsets.ofRaw(it.toInt()) }
}
else -> super.readValueOfType(type, buffer)
}
}
Expand All @@ -1142,6 +1169,10 @@ private open class AndroidWebkitLibraryPigeonCodec : StandardMessageCodec() {
stream.write(133)
writeValue(stream, value.raw.toLong())
}
is WindowInsets -> {
stream.write(134)
writeValue(stream, value.raw.toLong())
}
else -> super.writeValue(stream, value)
}
}
Expand Down Expand Up @@ -5316,6 +5347,20 @@ abstract class PigeonApiView(
/** Set the over-scroll mode for this view. */
abstract fun setOverScrollMode(pigeon_instance: android.view.View, mode: OverScrollMode)

/**
* Sets the listener to the native method `ViewCompat.setOnApplyWindowInsetsListener` to mark the
* passed insets to zero.
*
* Sets the padding of the view to match the insets passed.
*
* This is a convenience method because `View.OnApplyWindowInsetsListener` requires implementing a
* callback that requires a synchronous return value.
*/
abstract fun setInsetListenerToSetInsetsToZero(
pigeon_instance: android.view.View,
insets: List<WindowInsets>
)

companion object {
@Suppress("LocalVariableName")
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiView?) {
Expand Down Expand Up @@ -5460,6 +5505,30 @@ abstract class PigeonApiView(
channel.setMessageHandler(null)
}
}
run {
val channel =
BasicMessageChannel<Any?>(
binaryMessenger,
"dev.flutter.pigeon.webview_flutter_android.View.setInsetListenerToSetInsetsToZero",
codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val pigeon_instanceArg = args[0] as android.view.View
val insetsArg = args[1] as List<WindowInsets>
val wrapped: List<Any?> =
try {
api.setInsetListenerToSetInsetsToZero(pigeon_instanceArg, insetsArg)
listOf(null)
} catch (exception: Throwable) {
AndroidWebkitLibraryPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.List;

/**
* Flutter API implementation for `View`.
Expand Down Expand Up @@ -67,4 +71,36 @@ public void setOverScrollMode(@NonNull View pigeon_instance, @NonNull OverScroll
throw getPigeonRegistrar().createUnknownEnumException(OverScrollMode.UNKNOWN);
}
}

@Override
public void setInsetListenerToSetInsetsToZero(
@NonNull View pigeon_instance, @NonNull List<? extends WindowInsets> insets) {
int insetsTypeMask = 0;
for (WindowInsets inset : insets) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add comment here that indicates why SYTEM_BARS and DISPLAY_CUTOUT where chosen and how to know when a new item should be added to this list.

Additionally why use the all caps ints instead of WindowInsets.Type.systemBars()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only added those two because they were the only ones that we are interested in using for this opt out. I went ahead and added the remaining values for the enum. The caps for SYSTEM_BARS, DISPLAY_CUTOUT, etc. are the generated names for the WindowInsets enum created by pigeon.

I just changed them to WindowInsetsType to make it more accurate though.

switch (inset) {
case SYSTEM_BARS:
insetsTypeMask |= WindowInsetsCompat.Type.systemBars();
break;
case DISPLAY_CUTOUT:
insetsTypeMask |= WindowInsetsCompat.Type.displayCutout();
break;
}
}

final int finalTypeMask = insetsTypeMask;
ViewCompat.setOnApplyWindowInsetsListener(
pigeon_instance,
(view, windowInsets) -> {
if (finalTypeMask == 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining that this is checking to see if we set any bitwise values to make the code easier to skim without having someone need to understand why we are comparing to zero.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the logic to not need to make this check since it is bug prone. Now the code checks if the list is empty and handles it at the top.

return windowInsets;
}

final Insets allInsets = windowInsets.getInsets(finalTypeMask);
Comment thread
bparrishMines marked this conversation as resolved.
Outdated
pigeon_instance.setPadding(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does setting the padding here on the pigeon_instance do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I reread the opt out doc and my interpretation is that the padding used in that doc were just an example of how to manually handle the insets. So I removed the padding added in this PR since these seem to be handled by Flutter. The fix still works without it.

allInsets.left, allInsets.top, allInsets.right, allInsets.bottom);
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(finalTypeMask, Insets.NONE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line is saying to copy the existing windowInsets then to set all the ones we care about "consuming" to Insets.NONE.

If that is correct i think this line could be made more readable with the following changes.

Suggested change
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(finalTypeMask, Insets.NONE)
// Consume insects that are already handled by flutter.
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(activeInsetsHandledByFlutter, Insets.NONE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally if we cant define a list to import in both locations I think the code that is "already" handling the insets should be cross linked. As a result the case statements probably become another loop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code isn't intended to indicate what is handled by Flutter. I added comments about the selected inset types in android_webview_controller.dart.

My original idea was to expose this setting to the user, so they can have control over this. But I changed it to only handle this internally in the plugin instead for now since this may be a general change that we need to make for every WebView to work properly within Flutter.

.build();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The comment in android_webview_controller.dart states that "Flutter handles these internally", and the PR description mentions that the desired behavior is similar to iOS's contentInsetAdjustmentBehavior = .never. Both of these suggest that the web content should extend into the safe areas (i.e., underlap system bars and cutouts).

However, applying padding to the native View here will shrink the web content area, preventing it from drawing into the safe areas. This seems to contradict the intended goal of letting Flutter manage the safe area.

If the intention is to let Flutter handle safe areas, you should probably only consume the insets to prevent the WebView from applying them, but not apply any padding to the View itself. This would allow the web content to render edge-to-edge.

Could you clarify if this padding is intended? If not, these lines should be removed, and the corresponding test in ViewTest.java should be updated.

Suggested change
final Insets allInsets = windowInsets.getInsets(finalTypeMask);
pigeon_instance.setPadding(
allInsets.left, allInsets.top, allInsets.right, allInsets.bottom);
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(finalTypeMask, Insets.NONE)
.build();
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(finalTypeMask, Insets.NONE)
.build();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that an edge-edge WebView still works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm the behavior of a full screen flutter web view on a device with a simulated or actual tall cutout.
You can test on any physical device (or maybe emulator) by running adb shell cmd overlay enable com.android.internal.display.cutout.emulation.tall

});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
package io.flutter.plugins.webviewflutter;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.view.View;
import androidx.core.graphics.Insets;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.Collections;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;

public class ViewTest {
@Test
Expand Down Expand Up @@ -82,4 +91,29 @@ public void setOverScrollMode() {

verify(instance).setOverScrollMode(View.OVER_SCROLL_ALWAYS);
}

@Test
public void setInsetListenerToSetInsetsToZero() {
final PigeonApiView api = new TestProxyApiRegistrar().getPigeonApiView();

final View instance = mock(View.class);
final WindowInsetsCompat windowInsets = mock(WindowInsetsCompat.class);
final Insets insets = Insets.of(1, 2, 3, 4);

when(windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())).thenReturn(insets);

try (MockedStatic<ViewCompat> viewCompatMockedStatic = mockStatic(ViewCompat.class)) {
api.setInsetListenerToSetInsetsToZero(
instance, Collections.singletonList(WindowInsets.SYSTEM_BARS));

final ArgumentCaptor<OnApplyWindowInsetsListener> listenerCaptor =
ArgumentCaptor.forClass(OnApplyWindowInsetsListener.class);
viewCompatMockedStatic.verify(
() -> ViewCompat.setOnApplyWindowInsetsListener(eq(instance), listenerCaptor.capture()));

listenerCaptor.getValue().onApplyWindowInsets(instance, windowInsets);

verify(instance).setPadding(insets.left, insets.top, insets.right, insets.bottom);
}
}
}
Loading
Loading