Skip to content

Commit

Permalink
[Android R] Integrate DisplayCutouts into viewportMetrics (flutter#20921
Browse files Browse the repository at this point in the history
)
  • Loading branch information
GaryQian authored Sep 2, 2020
1 parent f6270c0 commit 15bf1bb
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 53 deletions.
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();
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
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;
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();
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));
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

0 comments on commit 15bf1bb

Please sign in to comment.