From cfe1db11469f48c4fe99a2bf766315fcf7ee0016 Mon Sep 17 00:00:00 2001 From: Gary Qian Date: Wed, 26 Aug 2020 18:15:40 -0700 Subject: [PATCH] Use Android R (API 30) getInsets() to compute padding (flutter/engine#18339) --- .../android/embedding_bundle/build.gradle | 2 +- .../embedding/android/FlutterView.java | 86 +++++++++----- .../embedding/android/FlutterViewTest.java | 107 +++++++++++++++++- .../scenario_app/android/app/build.gradle | 2 +- 4 files changed, 164 insertions(+), 33 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/embedding_bundle/build.gradle b/engine/src/flutter/shell/platform/android/embedding_bundle/build.gradle index c210e896527..7a60d4c16a2 100644 --- a/engine/src/flutter/shell/platform/android/embedding_bundle/build.gradle +++ b/engine/src/flutter/shell/platform/android/embedding_bundle/build.gradle @@ -37,7 +37,7 @@ configurations { } android { - compileSdkVersion 29 + compileSdkVersion 30 dependencies { embedding "androidx.annotation:annotation:1.1.0" diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 860d9c47860..aa7dd43a77d 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -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; diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 80b36824280..28c12f831af 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -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 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 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); diff --git a/engine/src/flutter/testing/scenario_app/android/app/build.gradle b/engine/src/flutter/testing/scenario_app/android/app/build.gradle index 44915f7d705..a2313057b9c 100644 --- a/engine/src/flutter/testing/scenario_app/android/app/build.gradle +++ b/engine/src/flutter/testing/scenario_app/android/app/build.gradle @@ -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"