From 45e6cfd1ffcdf3f380bbf3f9bdab61ae68e93302 Mon Sep 17 00:00:00 2001 From: xster Date: Tue, 28 Apr 2020 12:21:37 -0700 Subject: [PATCH] remove top padding when system UI in fullscreen mode (#17985) --- .../embedding/android/FlutterView.java | 3 +- .../embedding/android/FlutterViewTest.java | 107 +++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 1d550bbc528..dbc2566afee 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -400,10 +400,11 @@ public class FlutterView extends FrameLayout { @SuppressLint({"InlinedApi", "NewApi"}) @NonNull public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { + boolean statusBarHidden = (SYSTEM_UI_FLAG_FULLSCREEN & getWindowSystemUiVisibility()) != 0; WindowInsets newInsets = super.onApplyWindowInsets(insets); // Status bar (top) and left/right system insets should partially obscure the content (padding). - viewportMetrics.paddingTop = insets.getSystemWindowInsetTop(); + viewportMetrics.paddingTop = statusBarHidden ? 0 : insets.getSystemWindowInsetTop(); viewportMetrics.paddingRight = insets.getSystemWindowInsetRight(); viewportMetrics.paddingBottom = 0; viewportMetrics.paddingLeft = insets.getSystemWindowInsetLeft(); diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 27f4c78d8f5..954ed42e321 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -8,9 +8,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -21,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -29,9 +34,16 @@ import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; -@Config(manifest = Config.NONE) +// TODO(xster): we have 2 versions of robolectric Android shadows in +// shell/platform/android/embedding_bundle/build.gradle. Remove the older +// org.robolectric:android-all:4.1.2_r1-robolectric-r1 which doesn't have the right shadow +// behaviors. +@Config(manifest = Config.NONE, sdk = 27) @RunWith(RobolectricTestRunner.class) +@TargetApi(27) public class FlutterViewTest { @Mock FlutterJNI mockFlutterJni; @Mock FlutterLoader mockFlutterLoader; @@ -188,4 +200,97 @@ public class FlutterViewTest { // Verify results. assertEquals(SettingsChannel.PlatformBrightness.dark, reportedBrightness.get()); } + + @Test + @Config( + shadows = { + FlutterViewTest.ShadowFullscreenView.class, + FlutterViewTest.ShadowFullscreenViewGroup.class + }) + public void setPaddingTopToZeroForFullscreenMode() { + FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + 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(100); + when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); + when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); + flutterView.onApplyWindowInsets(windowInsets); + + // Verify. + verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().paddingTop); + // Padding bottom is always 0. + assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(100, viewportMetricsCaptor.getValue().paddingLeft); + assertEquals(100, viewportMetricsCaptor.getValue().paddingRight); + } + + @Test + public void reportSystemInsetWhenNotFullscreen() { + // Without custom shadows, the default system ui visibility flags is 0. + FlutterView flutterView = new FlutterView(RuntimeEnvironment.application); + assertEquals(0, flutterView.getSystemUiVisibility()); + + 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(100); + when(windowInsets.getSystemWindowInsetLeft()).thenReturn(100); + when(windowInsets.getSystemWindowInsetRight()).thenReturn(100); + flutterView.onApplyWindowInsets(windowInsets); + + // Verify. + verify(flutterRenderer, times(2)).setViewportMetrics(viewportMetricsCaptor.capture()); + // Top padding is reported as-is. + assertEquals(100, viewportMetricsCaptor.getValue().paddingTop); + // Padding bottom is always 0. + assertEquals(0, viewportMetricsCaptor.getValue().paddingBottom); + assertEquals(100, viewportMetricsCaptor.getValue().paddingLeft); + assertEquals(100, viewportMetricsCaptor.getValue().paddingRight); + } + + /* + * A custom shadow that reports fullscreen flag for system UI visibility + */ + @Implements(View.class) + public static class ShadowFullscreenView { + @Implementation + public int getWindowSystemUiVisibility() { + return View.SYSTEM_UI_FLAG_FULLSCREEN; + } + } + + // ViewGroup is the first shadow in the type hierarchy for FlutterView. Shadows need to mimic + // production classes' view hierarchy. + @Implements(ViewGroup.class) + public static class ShadowFullscreenViewGroup extends ShadowFullscreenView {} }