Use Android R (API 30) getInsets() to compute padding (flutter/engine#18339)

This commit is contained in:
Gary Qian 2020-08-26 18:15:40 -07:00 committed by GitHub
parent e026de2840
commit cfe1db1146
4 changed files with 164 additions and 33 deletions

View File

@ -37,7 +37,7 @@ configurations {
}
android {
compileSdkVersion 29
compileSdkVersion 30
dependencies {
embedding "androidx.annotation:annotation:1.1.0"

View File

@ -461,7 +461,6 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return ZeroSides.NONE;
}
// TODO(garyq): Use new Android R getInsets API
// TODO(garyq): The keyboard detection may interact strangely with
// https://github.com/flutter/flutter/issues/22061
@ -469,6 +468,9 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
// be padded. When the on-screen keyboard is detected, we want to include the full inset
// but when the inset is just the hidden nav bar, we want to provide a zero inset so the space
// can be used.
//
// This method is replaced by Android API 30 (R/11) getInsets() method which can take the
// android.view.WindowInsets.Type.ime() flag to find the keyboard inset.
@TargetApi(20)
@RequiresApi(20)
private int guessBottomKeyboardInset(WindowInsets insets) {
@ -506,37 +508,61 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
WindowInsets newInsets = super.onApplyWindowInsets(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();
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();
}
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;
viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = finalInsets.bottom;
viewportMetrics.viewInsetLeft = 0;
} 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).
viewportMetrics.paddingTop = statusBarVisible ? insets.getSystemWindowInsetTop() : 0;
viewportMetrics.paddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
viewportMetrics.paddingBottom = 0;
viewportMetrics.paddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom =
navigationBarVisible
? insets.getSystemWindowInsetBottom()
: guessBottomKeyboardInset(insets);
viewportMetrics.viewInsetLeft = 0;
}
// Status bar (top) and left/right system insets should partially obscure the content (padding).
viewportMetrics.paddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop();
viewportMetrics.paddingRight =
zeroSides == ZeroSides.RIGHT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetRight();
viewportMetrics.paddingBottom = 0;
viewportMetrics.paddingLeft =
zeroSides == ZeroSides.LEFT || zeroSides == ZeroSides.BOTH
? 0
: insets.getSystemWindowInsetLeft();
// Bottom system inset (keyboard) should adjust scrollable bottom edge (inset).
viewportMetrics.viewInsetTop = 0;
viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom =
navigationBarHidden
? guessBottomKeyboardInset(insets)
: insets.getSystemWindowInsetBottom();
viewportMetrics.viewInsetLeft = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Insets systemGestureInsets = insets.getSystemGestureInsets();
viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;

View File

@ -4,6 +4,7 @@ import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@ -15,6 +16,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.media.Image;
import android.media.ImageReader;
import android.view.View;
@ -47,7 +49,7 @@ import org.robolectric.shadows.ShadowDisplay;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
@TargetApi(28)
@TargetApi(30)
public class FlutterViewTest {
@Mock FlutterJNI mockFlutterJni;
@Mock FlutterLoader mockFlutterLoader;
@ -375,6 +377,109 @@ public class FlutterViewTest {
assertEquals(100, 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 systemInsetGetInsetsFullscreen() {
RuntimeEnvironment.setQualifiers("+land");
FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext));
ShadowDisplay display =
Shadows.shadowOf(
((WindowManager)
RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay());
display.setRotation(3);
assertEquals(0, flutterView.getSystemUiVisibility());
when(flutterView.getWindowSystemUiVisibility())
.thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
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);
// Then we simulate the system applying a window inset.
WindowInsets windowInsets = mock(WindowInsets.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);
flutterView.onApplyWindowInsets(windowInsets);
verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture());
// Top padding is removed due to full screen.
assertEquals(0, viewportMetricsCaptor.getValue().paddingTop);
// Padding bottom is always 0.
assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom);
// Left padding is zero because the rotation is 270deg
assertEquals(0, viewportMetricsCaptor.getValue().paddingLeft);
assertEquals(100, viewportMetricsCaptor.getValue().paddingRight);
}
// This test uses the pre-API 30 Algorithm for window insets.
@Test
@TargetApi(29)
@Config(sdk = 29)
public void systemInsetGetInsetsFullscreenLegacy() {
RuntimeEnvironment.setQualifiers("+land");
FlutterView flutterView = spy(new FlutterView(RuntimeEnvironment.systemContext));
ShadowDisplay display =
Shadows.shadowOf(
((WindowManager)
RuntimeEnvironment.systemContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay());
display.setRotation(3);
assertEquals(0, flutterView.getSystemUiVisibility());
when(flutterView.getWindowSystemUiVisibility())
.thenReturn(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
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);
// Then we simulate the system applying a window inset.
WindowInsets windowInsets = mock(WindowInsets.class);
when(windowInsets.getSystemWindowInsetTop()).thenReturn(100);
when(windowInsets.getSystemWindowInsetBottom()).thenReturn(101);
when(windowInsets.getSystemWindowInsetLeft()).thenReturn(102);
when(windowInsets.getSystemWindowInsetRight()).thenReturn(103);
flutterView.onApplyWindowInsets(windowInsets);
verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture());
// Top padding is removed due to full screen.
assertEquals(0, viewportMetricsCaptor.getValue().paddingTop);
// Padding bottom is always 0.
assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom);
// Left padding is zero because the rotation is 270deg
assertEquals(0, viewportMetricsCaptor.getValue().paddingLeft);
assertEquals(103, viewportMetricsCaptor.getValue().paddingRight);
}
@Test
public void flutterImageView_acquiresImageAndInvalidates() {
final ImageReader mockReader = mock(ImageReader.class);

View File

@ -15,7 +15,7 @@ android {
defaultConfig {
applicationId "dev.flutter.scenarios"
minSdkVersion 18
targetSdkVersion 28
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "dev.flutter.TestRunner"