Skip to content

Commit 15bf1bb

Browse files
authored
[Android R] Integrate DisplayCutouts into viewportMetrics (flutter#20921)
1 parent f6270c0 commit 15bf1bb

File tree

3 files changed

+201
-53
lines changed

3 files changed

+201
-53
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import android.text.format.DateFormat;
1515
import android.util.AttributeSet;
1616
import android.util.SparseArray;
17+
import android.view.DisplayCutout;
1718
import android.view.KeyEvent;
1819
import android.view.MotionEvent;
1920
import android.view.PointerIcon;
@@ -508,6 +509,15 @@ private int guessBottomKeyboardInset(WindowInsets insets) {
508509
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
509510
WindowInsets newInsets = super.onApplyWindowInsets(insets);
510511

512+
// getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
513+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
514+
Insets systemGestureInsets = insets.getSystemGestureInsets();
515+
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
516+
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
517+
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
518+
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
519+
}
520+
511521
boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
512522
boolean navigationBarVisible =
513523
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;
@@ -520,18 +530,48 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
520530
if (statusBarVisible) {
521531
mask = mask | android.view.WindowInsets.Type.statusBars();
522532
}
523-
mask = mask | android.view.WindowInsets.Type.ime();
524-
525-
Insets finalInsets = insets.getInsets(mask);
526-
viewportMetrics.paddingTop = finalInsets.top;
527-
viewportMetrics.paddingRight = finalInsets.right;
528-
viewportMetrics.paddingBottom = 0;
529-
viewportMetrics.paddingLeft = finalInsets.left;
533+
Insets uiInsets = insets.getInsets(mask);
534+
viewportMetrics.paddingTop = uiInsets.top;
535+
viewportMetrics.paddingRight = uiInsets.right;
536+
viewportMetrics.paddingBottom = uiInsets.bottom;
537+
viewportMetrics.paddingLeft = uiInsets.left;
538+
539+
Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
540+
viewportMetrics.viewInsetTop = imeInsets.top;
541+
viewportMetrics.viewInsetRight = imeInsets.right;
542+
viewportMetrics.viewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
543+
viewportMetrics.viewInsetLeft = imeInsets.left;
544+
545+
Insets systemGestureInsets =
546+
insets.getInsets(android.view.WindowInsets.Type.systemGestures());
547+
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
548+
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
549+
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
550+
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
530551

531-
viewportMetrics.viewInsetTop = 0;
532-
viewportMetrics.viewInsetRight = 0;
533-
viewportMetrics.viewInsetBottom = finalInsets.bottom;
534-
viewportMetrics.viewInsetLeft = 0;
552+
// TODO(garyq): Expose the full rects of the display cutout.
553+
554+
// Take the max of the display cutout insets and existing padding to merge them
555+
DisplayCutout cutout = insets.getDisplayCutout();
556+
if (cutout != null) {
557+
Insets waterfallInsets = cutout.getWaterfallInsets();
558+
viewportMetrics.paddingTop =
559+
Math.max(
560+
Math.max(viewportMetrics.paddingTop, waterfallInsets.top),
561+
cutout.getSafeInsetTop());
562+
viewportMetrics.paddingRight =
563+
Math.max(
564+
Math.max(viewportMetrics.paddingRight, waterfallInsets.right),
565+
cutout.getSafeInsetRight());
566+
viewportMetrics.paddingBottom =
567+
Math.max(
568+
Math.max(viewportMetrics.paddingBottom, waterfallInsets.bottom),
569+
cutout.getSafeInsetBottom());
570+
viewportMetrics.paddingLeft =
571+
Math.max(
572+
Math.max(viewportMetrics.paddingLeft, waterfallInsets.left),
573+
cutout.getSafeInsetLeft());
574+
}
535575
} else {
536576
// We zero the left and/or right sides to prevent the padding the
537577
// navigation bar would have caused.
@@ -563,14 +603,6 @@ public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
563603
viewportMetrics.viewInsetLeft = 0;
564604
}
565605

566-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
567-
Insets systemGestureInsets = insets.getSystemGestureInsets();
568-
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
569-
viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
570-
viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
571-
viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
572-
}
573-
574606
Log.v(
575607
TAG,
576608
"Updating window insets (onApplyWindowInsets()):\n"

shell/platform/android/io/flutter/view/FlutterView.java

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.util.AttributeSet;
2222
import android.util.Log;
2323
import android.util.SparseArray;
24+
import android.view.DisplayCutout;
2425
import android.view.KeyEvent;
2526
import android.view.MotionEvent;
2627
import android.view.PointerIcon;
@@ -588,47 +589,100 @@ private int guessBottomKeyboardInset(WindowInsets insets) {
588589
@RequiresApi(20)
589590
@SuppressLint({"InlinedApi", "NewApi"})
590591
public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
591-
boolean statusBarHidden = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) != 0;
592-
boolean navigationBarHidden =
593-
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) != 0;
594-
595-
// We zero the left and/or right sides to prevent the padding the
596-
// navigation bar would have caused.
597-
ZeroSides zeroSides = ZeroSides.NONE;
598-
if (navigationBarHidden) {
599-
zeroSides = calculateShouldZeroSides();
592+
// getSystemGestureInsets() was introduced in API 29 and immediately deprecated in 30.
593+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
594+
Insets systemGestureInsets = insets.getSystemGestureInsets();
595+
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
596+
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
597+
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
598+
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;
600599
}
601600

602-
// The padding on top should be removed when the statusbar is hidden.
603-
mMetrics.physicalPaddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop();
604-
mMetrics.physicalPaddingRight =
605-
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
606-
? 0
607-
: insets.getSystemWindowInsetRight();
608-
mMetrics.physicalPaddingBottom = 0;
609-
mMetrics.physicalPaddingLeft =
610-
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
611-
? 0
612-
: insets.getSystemWindowInsetLeft();
613-
614-
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
615-
mMetrics.physicalViewInsetTop = 0;
616-
mMetrics.physicalViewInsetRight = 0;
617-
// We perform hidden navbar and keyboard handling if the navbar is set to hidden. Otherwise,
618-
// the navbar padding should always be provided.
619-
mMetrics.physicalViewInsetBottom =
620-
navigationBarHidden
621-
? guessBottomKeyboardInset(insets)
622-
: insets.getSystemWindowInsetBottom();
623-
mMetrics.physicalViewInsetLeft = 0;
624-
625-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
626-
Insets systemGestureInsets = insets.getSystemGestureInsets();
601+
boolean statusBarVisible = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) == 0;
602+
boolean navigationBarVisible =
603+
(SYSTEM_UI_FLAG_HIDE_NAVIGATION & getWindowSystemUiVisibility()) == 0;
604+
605+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
606+
int mask = 0;
607+
if (navigationBarVisible) {
608+
mask = mask | android.view.WindowInsets.Type.navigationBars();
609+
}
610+
if (statusBarVisible) {
611+
mask = mask | android.view.WindowInsets.Type.statusBars();
612+
}
613+
Insets uiInsets = insets.getInsets(mask);
614+
mMetrics.physicalPaddingTop = uiInsets.top;
615+
mMetrics.physicalPaddingRight = uiInsets.right;
616+
mMetrics.physicalPaddingBottom = uiInsets.bottom;
617+
mMetrics.physicalPaddingLeft = uiInsets.left;
618+
619+
Insets imeInsets = insets.getInsets(android.view.WindowInsets.Type.ime());
620+
mMetrics.physicalViewInsetTop = imeInsets.top;
621+
mMetrics.physicalViewInsetRight = imeInsets.right;
622+
mMetrics.physicalViewInsetBottom = imeInsets.bottom; // Typically, only bottom is non-zero
623+
mMetrics.physicalViewInsetLeft = imeInsets.left;
624+
625+
Insets systemGestureInsets =
626+
insets.getInsets(android.view.WindowInsets.Type.systemGestures());
627627
mMetrics.systemGestureInsetTop = systemGestureInsets.top;
628628
mMetrics.systemGestureInsetRight = systemGestureInsets.right;
629629
mMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
630630
mMetrics.systemGestureInsetLeft = systemGestureInsets.left;
631+
632+
// TODO(garyq): Expose the full rects of the display cutout.
633+
634+
// Take the max of the display cutout insets and existing padding to merge them
635+
DisplayCutout cutout = insets.getDisplayCutout();
636+
if (cutout != null) {
637+
Insets waterfallInsets = cutout.getWaterfallInsets();
638+
mMetrics.physicalPaddingTop =
639+
Math.max(
640+
Math.max(mMetrics.physicalPaddingTop, waterfallInsets.top),
641+
cutout.getSafeInsetTop());
642+
mMetrics.physicalPaddingRight =
643+
Math.max(
644+
Math.max(mMetrics.physicalPaddingRight, waterfallInsets.right),
645+
cutout.getSafeInsetRight());
646+
mMetrics.physicalPaddingBottom =
647+
Math.max(
648+
Math.max(mMetrics.physicalPaddingBottom, waterfallInsets.bottom),
649+
cutout.getSafeInsetBottom());
650+
mMetrics.physicalPaddingLeft =
651+
Math.max(
652+
Math.max(mMetrics.physicalPaddingLeft, waterfallInsets.left),
653+
cutout.getSafeInsetLeft());
654+
}
655+
} else {
656+
// We zero the left and/or right sides to prevent the padding the
657+
// navigation bar would have caused.
658+
ZeroSides zeroSides = ZeroSides.NONE;
659+
if (!navigationBarVisible) {
660+
zeroSides = calculateShouldZeroSides();
661+
}
662+
663+
// Status bar (top) and left/right system insets should partially obscure the content
664+
// (padding).
665+
mMetrics.physicalPaddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0;
666+
mMetrics.physicalPaddingRight =
667+
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
668+
? 0
669+
: insets.getSystemWindowInsetRight();
670+
mMetrics.physicalPaddingBottom = 0;
671+
mMetrics.physicalPaddingLeft =
672+
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
673+
? 0
674+
: insets.getSystemWindowInsetLeft();
675+
676+
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
677+
mMetrics.physicalViewInsetTop = 0;
678+
mMetrics.physicalViewInsetRight = 0;
679+
mMetrics.physicalViewInsetBottom =
680+
navigationBarVisible
681+
? insets.getSystemWindowInsetBottom()
682+
: guessBottomKeyboardInset(insets);
683+
mMetrics.physicalViewInsetLeft = 0;
631684
}
685+
632686
updateViewportMetrics();
633687
return super.onApplyWindowInsets(insets);
634688
}

shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import android.media.Image;
2323
import android.media.Image.Plane;
2424
import android.media.ImageReader;
25+
import android.view.DisplayCutout;
2526
import android.view.View;
2627
import android.view.ViewGroup;
2728
import android.view.WindowInsets;
@@ -483,6 +484,67 @@ public void systemInsetGetInsetsFullscreenLegacy() {
483484
assertEquals(103, viewportMetricsCaptor.getValue().paddingRight);
484485
}
485486

487+
// This test uses the API 30+ Algorithm for window insets. The legacy algorithm is
488+
// set to -1 values, so it is clear if the wrong algorithm is used.
489+
@Test
490+
@TargetApi(30)
491+
@Config(sdk = 30)
492+
public void systemInsetDisplayCutoutSimple() {
493+
RuntimeEnvironment.setQualifiers("+land");
494+
FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext));
495+
ShadowDisplay display =
496+
Shadows.shadowOf(
497+
((WindowManager)
498+
RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE))
499+
.getDefaultDisplay());
500+
assertEquals(0, flutterView.getSystemUiVisibility());
501+
when(flutterView.getWindowSystemUiVisibility()).thenReturn(0);
502+
when(flutterView.getContext()).thenReturn(RuntimeEnvironment.systemContext);
503+
504+
FlutterEngine flutterEngine =
505+
spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
506+
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
507+
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);
508+
509+
// When we attach a new FlutterView to the engine without any system insets,
510+
// the viewport metrics default to 0.
511+
flutterView.attachToFlutterEngine(flutterEngine);
512+
ArgumentCaptor<FlutterRenderer.ViewportMetrics> viewportMetricsCaptor =
513+
ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class);
514+
verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
515+
assertEquals(0, viewportMetricsCaptor.getValue().paddingTop);
516+
517+
Insets insets = Insets.of(100, 100, 100, 100);
518+
Insets systemGestureInsets = Insets.of(110, 110, 110, 110);
519+
// Then we simulate the system applying a window inset.
520+
WindowInsets windowInsets = mock(WindowInsets.class);
521+
DisplayCutout displayCutout = mock(DisplayCutout.class);
522+
when(windowInsets.getSystemWindowInsetTop()).thenReturn(-1);
523+
when(windowInsets.getSystemWindowInsetBottom()).thenReturn(-1);
524+
when(windowInsets.getSystemWindowInsetLeft()).thenReturn(-1);
525+
when(windowInsets.getSystemWindowInsetRight()).thenReturn(-1);
526+
when(windowInsets.getInsets(anyInt())).thenReturn(insets);
527+
when(windowInsets.getSystemGestureInsets()).thenReturn(systemGestureInsets);
528+
when(windowInsets.getDisplayCutout()).thenReturn(displayCutout);
529+
530+
Insets waterfallInsets = Insets.of(200, 0, 200, 0);
531+
when(displayCutout.getWaterfallInsets()).thenReturn(waterfallInsets);
532+
when(displayCutout.getSafeInsetTop()).thenReturn(150);
533+
when(displayCutout.getSafeInsetBottom()).thenReturn(150);
534+
when(displayCutout.getSafeInsetLeft()).thenReturn(150);
535+
when(displayCutout.getSafeInsetRight()).thenReturn(150);
536+
537+
flutterView.onApplyWindowInsets(windowInsets);
538+
539+
verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture());
540+
assertEquals(150, viewportMetricsCaptor.getValue().paddingTop);
541+
assertEquals(150, viewportMetricsCaptor.getValue().paddingBottom);
542+
assertEquals(200, viewportMetricsCaptor.getValue().paddingLeft);
543+
assertEquals(200, viewportMetricsCaptor.getValue().paddingRight);
544+
545+
assertEquals(100, viewportMetricsCaptor.getValue().viewInsetTop);
546+
}
547+
486548
@Test
487549
public void flutterImageView_acquiresImageAndInvalidates() {
488550
final ImageReader mockReader = mock(ImageReader.class);

0 commit comments

Comments
 (0)