Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.DisplayCutout;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
Expand Down Expand Up @@ -508,6 +509,15 @@ private int guessBottomKeyboardInset(WindowInsets insets) {
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
WindowInsets newInsets = super.onApplyWindowInsets(insets);

// getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
Copy link
Member

Choose a reason for hiding this comment

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

We don't have to do this now, but this got deprecated after 1 Android version 🤣. We could add an R check for the new API.

Copy link
Member

Choose a reason for hiding this comment

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

Oh you're already doing getInsets below. If that works in Q, we should just merge it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, getInsets is API 30 only, so to support this for Q, we still need to do this. For R and above though, we can move to getInsets

Copy link
Member

Choose a reason for hiding this comment

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

Yup, SG

viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
Copy link
Member

Choose a reason for hiding this comment

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

Super tangential, just realized we added this to mediaquery. We should move the iOS home bar to use this too :)

Copy link
Member

Choose a reason for hiding this comment

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

sorry disregard. After reading this from media query and android, I realized it doesn't mean the same thing

viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
}

boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
boolean navigationBarVisible =
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;
Copy link
Member

Choose a reason for hiding this comment

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

These things are kinda big now. Feel free to split them into populatePreQ, populateQ, populateR if needed.

Expand All @@ -520,18 +530,48 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
if (statusBarVisible) {
mask = mask | android.view.WindowInsets.Type.statusBars();
}
mask = mask | android.view.WindowInsets.Type.ime();

Insets finalInsets = insets.getInsets(mask);
viewportMetrics.paddingTop = finalInsets.top;
viewportMetrics.paddingRight = finalInsets.right;
viewportMetrics.paddingBottom = 0;
viewportMetrics.paddingLeft = finalInsets.left;
Insets uiInsets = insets.getInsets(mask);
viewportMetrics.paddingTop = uiInsets.top;
viewportMetrics.paddingRight = uiInsets.right;
viewportMetrics.paddingBottom = uiInsets.bottom;
viewportMetrics.paddingLeft = uiInsets.left;

Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
viewportMetrics.viewInsetTop = imeInsets.top;
viewportMetrics.viewInsetRight = imeInsets.right;
viewportMetrics.viewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
viewportMetrics.viewInsetLeft = imeInsets.left;

Insets systemGestureInsets =
insets.getInsets(android.view.WindowInsets.Type.systemGestures());
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;

viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = finalInsets.bottom;
viewportMetrics.viewInsetLeft = 0;
// TODO(garyq): Expose the full rects of the display cutout.

// Take the max of the display cutout insets and existing padding to merge them
DisplayCutout cutout = insets.getDisplayCutout();
Copy link
Member

Choose a reason for hiding this comment

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

Oh, neat that they have the insets apis separate from the rect apis. I suppose there's no regression here since on iOS, you already can't tell apart the notch rect from the insets to "utilize" the remaining area.

Let's do the rect separately, sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

flutter/flutter#65088 Tracks the rect work

if (cutout != null) {
Insets waterfallInsets = cutout.getWaterfallInsets();
viewportMetrics.paddingTop =
Math.max(
Math.max(viewportMetrics.paddingTop, waterfallInsets.top),
cutout.getSafeInsetTop());
viewportMetrics.paddingRight =
Math.max(
Math.max(viewportMetrics.paddingRight, waterfallInsets.right),
cutout.getSafeInsetRight());
viewportMetrics.paddingBottom =
Math.max(
Math.max(viewportMetrics.paddingBottom, waterfallInsets.bottom),
cutout.getSafeInsetBottom());
viewportMetrics.paddingLeft =
Math.max(
Math.max(viewportMetrics.paddingLeft, waterfallInsets.left),
cutout.getSafeInsetLeft());
}
} else {
// We zero the left and/or right sides to prevent the padding the
// navigation bar would have caused.
Expand Down Expand Up @@ -563,14 +603,6 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
viewportMetrics.viewInsetLeft = 0;
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
}

Log.v(
TAG,
"Updating window insets (onApplyWindowInsets()):\n"
Expand Down
122 changes: 88 additions & 34 deletions shell/platform/android/io/flutter/view/FlutterView.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.DisplayCutout;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
Expand Down Expand Up @@ -588,47 +589,100 @@ private int guessBottomKeyboardInset(WindowInsets insets) {
@RequiresApi(20)
@SuppressLint({"InlinedApi", "NewApi"})
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
boolean statusBarHidden = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) != 0;
boolean navigationBarHidden =
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) != 0;

// We zero the left and/or right sides to prevent the padding the
// navigation bar would have caused.
ZeroSides zeroSides = ZeroSides.NONE;
if (navigationBarHidden) {
zeroSides = calculateShouldZeroSides();
// getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;
}

// The padding on top should be removed when the statusbar is hidden.
mMetrics.physicalPaddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop();
mMetrics.physicalPaddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
mMetrics.physicalPaddingBottom = 0;
mMetrics.physicalPaddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();

// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
mMetrics.physicalViewInsetTop = 0;
mMetrics.physicalViewInsetRight = 0;
// We perform hidden navbar and keyboard handling if the navbar is set to hidden. Otherwise,
// the navbar padding should always be provided.
mMetrics.physicalViewInsetBottom =
navigationBarHidden
? guessBottomKeyboardInset(insets)
: insets.getSystemWindowInsetBottom();
mMetrics.physicalViewInsetLeft = 0;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
boolean navigationBarVisible =
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
int mask = 0;
if (navigationBarVisible) {
mask = mask | android.view.WindowInsets.Type.navigationBars();
}
if (statusBarVisible) {
mask = mask | android.view.WindowInsets.Type.statusBars();
}
Insets uiInsets = insets.getInsets(mask);
mMetrics.physicalPaddingTop = uiInsets.top;
mMetrics.physicalPaddingRight = uiInsets.right;
mMetrics.physicalPaddingBottom = uiInsets.bottom;
mMetrics.physicalPaddingLeft = uiInsets.left;

Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
mMetrics.physicalViewInsetTop = imeInsets.top;
mMetrics.physicalViewInsetRight = imeInsets.right;
mMetrics.physicalViewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
mMetrics.physicalViewInsetLeft = imeInsets.left;

Insets systemGestureInsets =
insets.getInsets(android.view.WindowInsets.Type.systemGestures());
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;

// TODO(garyq): Expose the full rects of the display cutout.

// Take the max of the display cutout insets and existing padding to merge them
DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
Insets waterfallInsets = cutout.getWaterfallInsets();
mMetrics.physicalPaddingTop =
Math.max(
Math.max(mMetrics.physicalPaddingTop, waterfallInsets.top),
cutout.getSafeInsetTop());
mMetrics.physicalPaddingRight =
Math.max(
Math.max(mMetrics.physicalPaddingRight, waterfallInsets.right),
cutout.getSafeInsetRight());
mMetrics.physicalPaddingBottom =
Math.max(
Math.max(mMetrics.physicalPaddingBottom, waterfallInsets.bottom),
cutout.getSafeInsetBottom());
mMetrics.physicalPaddingLeft =
Math.max(
Math.max(mMetrics.physicalPaddingLeft, waterfallInsets.left),
cutout.getSafeInsetLeft());
}
} else {
// We zero the left and/or right sides to prevent the padding the
// navigation bar would have caused.
ZeroSides zeroSides = ZeroSides.NONE;
if (!navigationBarVisible) {
zeroSides = calculateShouldZeroSides();
}

// Status bar (top) and left/right system insets should partially obscure the content
// (padding).
mMetrics.physicalPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0;
mMetrics.physicalPaddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
mMetrics.physicalPaddingBottom = 0;
mMetrics.physicalPaddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();

// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
mMetrics.physicalViewInsetTop = 0;
mMetrics.physicalViewInsetRight = 0;
mMetrics.physicalViewInsetBottom =
navigationBarVisible
? insets.getSystemWindowInsetBottom()
: guessBottomKeyboardInset(insets);
mMetrics.physicalViewInsetLeft = 0;
}

updateViewportMetrics();
return super.onApplyWindowInsets(insets);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.media.Image;
import android.media.Image.Plane;
import android.media.ImageReader;
import android.view.DisplayCutout;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
Expand Down Expand Up @@ -483,6 +484,67 @@ public void systemInsetGetInsetsFullscreenLegacy() {
assertEquals(103, viewportMetricsCaptor.getValue().paddingRight);
}

// This test uses the API 30+ Algorithm for window insets. The legacy algorithm is
// set to -1 values, so it is clear if the wrong algorithm is used.
@Test
@TargetApi(30)
@Config(sdk = 30)
public void systemInsetDisplayCutoutSimple() {
RuntimeEnvironment.setQualifiers("+land");
FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext));
ShadowDisplay display =
Shadows.shadowOf(
((WindowManager)
RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay());
assertEquals(0, flutterView.getSystemUiVisibility());
when(flutterView.getWindowSystemUiVisibility()).thenReturn(0);
when(flutterView.getContext()).thenReturn(RuntimeEnvironment.systemContext);

FlutterEngine flutterEngine =
spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
Copy link
Member

Choose a reason for hiding this comment

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

ultra nit: if you can figure out the chain needed to just mock instead of spy, it might make this test more stable in the long run. Might not be possible though.

FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);

// When we attach a new FlutterView to the engine without any system insets,
// the viewport metrics default to 0.
flutterView.attachToFlutterEngine(flutterEngine);
ArgumentCaptor<FlutterRenderer.ViewportMetrics> viewportMetricsCaptor =
ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class);
verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(0, viewportMetricsCaptor.getValue().paddingTop);

Insets insets = Insets.of(100, 100, 100, 100);
Insets systemGestureInsets = Insets.of(110, 110, 110, 110);
// Then we simulate the system applying a window inset.
WindowInsets windowInsets = mock(WindowInsets.class);
DisplayCutout displayCutout = mock(DisplayCutout.class);
when(windowInsets.getSystemWindowInsetTop()).thenReturn(-1);
when(windowInsets.getSystemWindowInsetBottom()).thenReturn(-1);
when(windowInsets.getSystemWindowInsetLeft()).thenReturn(-1);
when(windowInsets.getSystemWindowInsetRight()).thenReturn(-1);
when(windowInsets.getInsets(anyInt())).thenReturn(insets);
when(windowInsets.getSystemGestureInsets()).thenReturn(systemGestureInsets);
when(windowInsets.getDisplayCutout()).thenReturn(displayCutout);

Insets waterfallInsets = Insets.of(200, 0, 200, 0);
when(displayCutout.getWaterfallInsets()).thenReturn(waterfallInsets);
when(displayCutout.getSafeInsetTop()).thenReturn(150);
when(displayCutout.getSafeInsetBottom()).thenReturn(150);
when(displayCutout.getSafeInsetLeft()).thenReturn(150);
when(displayCutout.getSafeInsetRight()).thenReturn(150);

flutterView.onApplyWindowInsets(windowInsets);

verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(150, viewportMetricsCaptor.getValue().paddingTop);
assertEquals(150, viewportMetricsCaptor.getValue().paddingBottom);
assertEquals(200, viewportMetricsCaptor.getValue().paddingLeft);
assertEquals(200, viewportMetricsCaptor.getValue().paddingRight);

assertEquals(100, viewportMetricsCaptor.getValue().viewInsetTop);
}

@Test
public void flutterImageView_acquiresImageAndInvalidates() {
final ImageReader mockReader = mock(ImageReader.class);
Expand Down