-
Notifications
You must be signed in to change notification settings - Fork 6k
[Android R] Integrate DisplayCutouts into viewportMetrics #20921
Changes from all commits
7281731
036aa8c
8fb6c94
7e26c7d
56fb061
9504f64
b3cc87f
88e279f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
@@ -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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, SG