diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 060221650243f..16497674240de 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -542,7 +542,8 @@ private View createFlutterView() { /* inflater=*/ null, /* container=*/ null, /* savedInstanceState=*/ null, - /*flutterViewId=*/ FLUTTER_VIEW_ID); + /*flutterViewId=*/ FLUTTER_VIEW_ID, + /*shouldDelayFirstAndroidViewDraw=*/ getRenderMode() == RenderMode.surface); } private void configureStatusBarForFullscreenFlutterExperience() { diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index b6f5eb465a03d..d153a26d7cfc8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -15,6 +15,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnPreDrawListener; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -67,15 +68,17 @@ private static final String TAG = "FlutterActivityAndFragmentDelegate"; private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework"; private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins"; + private static final int FLUTTER_SPLASH_VIEW_FALLBACK_ID = 486947586; // The FlutterActivity or FlutterFragment that is delegating most of its calls // to this FlutterActivityAndFragmentDelegate. @NonNull private Host host; @Nullable private FlutterEngine flutterEngine; - @Nullable private FlutterSplashView flutterSplashView; @Nullable private FlutterView flutterView; @Nullable private PlatformPlugin platformPlugin; + @VisibleForTesting @Nullable OnPreDrawListener activePreDrawListener; private boolean isFlutterEngineFromHost; + private boolean isFlutterUiDisplayed; @NonNull private final FlutterUiDisplayListener flutterUiDisplayListener = @@ -83,11 +86,13 @@ @Override public void onFlutterUiDisplayed() { host.onFlutterUiDisplayed(); + isFlutterUiDisplayed = true; } @Override public void onFlutterUiNoLongerDisplayed() { host.onFlutterUiNoLongerDisplayed(); + isFlutterUiDisplayed = false; } }; @@ -254,6 +259,16 @@ void onAttach(@NonNull Context context) { * *
{@code inflater} and {@code container} may be null when invoked from an {@code Activity}. * + *
{@code shouldDelayFirstAndroidViewDraw} determines whether to set up an {@link + * android.view.ViewTreeObserver.OnPreDrawListener}, which will defer the current drawing pass + * till after the Flutter UI has been displayed. This results in more accurate timings reported + * with Android tools, such as "Displayed" timing printed with `am start`. + * + *
Note that it should only be set to true when {@code Host#getRenderMode()} is {@code + * RenderMode.surface}. This parameter is also ignored, disabling the delay should the legacy + * {@code Host#provideSplashScreen()} be non-null. See Android Splash Migration. + * *
This method: * *
See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details. + */ + @NonNull + public NewEngineFragmentBuilder shouldDelayFirstAndroidViewDraw( + boolean shouldDelayFirstAndroidViewDraw) { + this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw; + return this; + } + /** * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. * @@ -410,6 +427,7 @@ protected Bundle createArgs() { args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); args.putBoolean( ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed); + args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw); return args; } @@ -496,6 +514,7 @@ public static class CachedEngineFragmentBuilder { private TransparencyMode transparencyMode = TransparencyMode.transparent; private boolean shouldAttachEngineToActivity = true; private boolean shouldAutomaticallyHandleOnBackPressed = false; + private boolean shouldDelayFirstAndroidViewDraw = false; private CachedEngineFragmentBuilder(@NonNull String engineId) { this(FlutterFragment.class, engineId); @@ -621,6 +640,18 @@ public CachedEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed( return this; } + /** + * Whether to delay the Android drawing pass till after the Flutter UI has been displayed. + * + *
See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details.
+ */
+ @NonNull
+ public CachedEngineFragmentBuilder shouldDelayFirstAndroidViewDraw(
+ @NonNull boolean shouldDelayFirstAndroidViewDraw) {
+ this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw;
+ return this;
+ }
+
/**
* Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}.
*
@@ -642,6 +673,7 @@ protected Bundle createArgs() {
args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity);
args.putBoolean(
ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed);
+ args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw);
return args;
}
@@ -727,7 +759,11 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
public View onCreateView(
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return delegate.onCreateView(
- inflater, container, savedInstanceState, /*flutterViewId=*/ FLUTTER_VIEW_ID);
+ inflater,
+ container,
+ savedInstanceState,
+ /*flutterViewId=*/ FLUTTER_VIEW_ID,
+ shouldDelayFirstAndroidViewDraw());
}
@Override
@@ -1267,6 +1303,12 @@ public boolean popSystemNavigator() {
return false;
}
+ @VisibleForTesting
+ @NonNull
+ boolean shouldDelayFirstAndroidViewDraw() {
+ return getArguments().getBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW);
+ }
+
private boolean stillAttachedForEvent(String event) {
if (delegate == null) {
Log.w(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release.");
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
index 74e008448872f..52c65ca855d45 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java
@@ -424,6 +424,7 @@ protected FlutterFragment createFlutterFragment() {
backgroundMode == BackgroundMode.opaque
? TransparencyMode.opaque
: TransparencyMode.transparent;
+ final boolean shouldDelayFirstAndroidViewDraw = renderMode == RenderMode.surface;
if (getCachedEngineId() != null) {
Log.v(
@@ -447,6 +448,7 @@ protected FlutterFragment createFlutterFragment() {
.handleDeeplinking(shouldHandleDeeplinking())
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
.destroyEngineWithFragment(shouldDestroyEngineWithHost())
+ .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw)
.build();
} else {
Log.v(
@@ -476,6 +478,7 @@ protected FlutterFragment createFlutterFragment() {
.renderMode(renderMode)
.transparencyMode(transparencyMode)
.shouldAttachEngineToActivity(shouldAttachEngineToActivity())
+ .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw)
.build();
}
}
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index 5dfd509230e49..112b3736a3080 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -2,7 +2,9 @@
import static android.content.ComponentCallbacks2.*;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.notNull;
@@ -15,6 +17,8 @@
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
@@ -86,7 +90,7 @@ public void itSendsLifecycleEventsToFlutter() {
// We're testing lifecycle behaviors, which require/expect that certain methods have already
// been executed by the time they run. Therefore, we run those expected methods first.
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
// --- Execute the behavior under test ---
// By the time an Activity/Fragment is started, we don't expect any lifecycle messages
@@ -164,7 +168,7 @@ public void itUsesCachedEngineWhenProvided() {
// --- Execute the behavior under test ---
// The FlutterEngine is obtained in onAttach().
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
@@ -220,7 +224,7 @@ public void itGivesHostAnOpportunityToConfigureFlutterSurfaceView() {
// --- Execute the behavior under test ---
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
// Verify that the host was asked to configure a FlutterSurfaceView.
verify(mockHost, times(1)).onFlutterSurfaceViewCreated(notNull(FlutterSurfaceView.class));
@@ -249,7 +253,7 @@ public void itGivesHostAnOpportunityToConfigureFlutterTextureView() {
// --- Execute the behavior under test ---
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, false);
// Verify that the host was asked to configure a FlutterTextureView.
verify(customMockHost, times(1)).onFlutterTextureViewCreated(notNull(FlutterTextureView.class));
@@ -282,7 +286,7 @@ public void itSendsInitialRouteToFlutter() {
// --- Execute the behavior under test ---
// The initial route is sent in onStart().
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
// Verify that the navigation channel was given our initial route.
@@ -306,7 +310,7 @@ public void itExecutesDartEntrypointProvidedByHost() {
// --- Execute the behavior under test ---
// Dart is executed in onStart().
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
@@ -335,7 +339,7 @@ public void itUsesDefaultFlutterLoaderAppBundlePathWhenUnspecified() {
// --- Execute the behavior under test ---
// Dart is executed in onStart().
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
// Verify that the host's Dart entrypoint was used.
@@ -390,7 +394,7 @@ public void itDoesNotAttachFlutterToTheActivityIfNotDesired() {
// Make sure all of the other lifecycle methods can run safely as well
// without a valid Activity
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
delegate.onPause();
@@ -751,7 +755,7 @@ public void itDestroysItsOwnEngineIfHostRequestsIt() {
// --- Execute the behavior under test ---
// Push the delegate through all lifecycle methods all the way to destruction.
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
delegate.onPause();
@@ -775,7 +779,7 @@ public void itDoesNotDestroyItsOwnEngineWhenHostSaysNotTo() {
// --- Execute the behavior under test ---
// Push the delegate through all lifecycle methods all the way to destruction.
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
delegate.onPause();
@@ -806,7 +810,7 @@ public void itDestroysCachedEngineWhenHostRequestsIt() {
// --- Execute the behavior under test ---
// Push the delegate through all lifecycle methods all the way to destruction.
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
delegate.onPause();
@@ -838,7 +842,7 @@ public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() {
// --- Execute the behavior under test ---
// Push the delegate through all lifecycle methods all the way to destruction.
delegate.onAttach(RuntimeEnvironment.application);
- delegate.onCreateView(null, null, null, 0);
+ delegate.onCreateView(null, null, null, 0, true);
delegate.onStart();
delegate.onResume();
delegate.onPause();
@@ -850,6 +854,77 @@ public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() {
verify(cachedEngine, never()).destroy();
}
+ @Test
+ public void itDelaysFirstDrawWhenRequested() {
+ // ---- Test setup ----
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // We're testing lifecycle behaviors, which require/expect that certain methods have already
+ // been executed by the time they run. Therefore, we run those expected methods first.
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // --- Execute the behavior under test ---
+ boolean shouldDelayFirstAndroidViewDraw = true;
+ delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw);
+
+ assertNotNull(delegate.activePreDrawListener);
+ }
+
+ @Test
+ public void itDoesNotDelayFirstDrawWhenNotRequested() {
+ // ---- Test setup ----
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // We're testing lifecycle behaviors, which require/expect that certain methods have already
+ // been executed by the time they run. Therefore, we run those expected methods first.
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // --- Execute the behavior under test ---
+ boolean shouldDelayFirstAndroidViewDraw = false;
+ delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw);
+
+ assertNull(delegate.activePreDrawListener);
+ }
+
+ @Test
+ public void itThrowsWhenDelayingTheFirstDrawAndUsingATextureView() {
+ // ---- Test setup ----
+ when(mockHost.getRenderMode()).thenReturn(RenderMode.texture);
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // We're testing lifecycle behaviors, which require/expect that certain methods have already
+ // been executed by the time they run. Therefore, we run those expected methods first.
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // --- Execute the behavior under test ---
+ boolean shouldDelayFirstAndroidViewDraw = true;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw);
+ });
+ }
+
+ @Test
+ public void itDoesNotDelayTheFirstDrawWhenRequestedAndWithAProvidedSplashScreen() {
+ when(mockHost.provideSplashScreen())
+ .thenReturn(new DrawableSplashScreen(new ColorDrawable(Color.GRAY)));
+
+ // ---- Test setup ----
+ // Create the real object that we're testing.
+ FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost);
+
+ // We're testing lifecycle behaviors, which require/expect that certain methods have already
+ // been executed by the time they run. Therefore, we run those expected methods first.
+ delegate.onAttach(RuntimeEnvironment.application);
+
+ // --- Execute the behavior under test ---
+ boolean shouldDelayFirstAndroidViewDraw = true;
+ delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw);
+
+ assertNull(delegate.activePreDrawListener);
+ }
+
/**
* Creates a mock {@link io.flutter.embedding.engine.FlutterEngine}.
*
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
index ae42af77c5c38..b88460b3cb423 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java
@@ -291,6 +291,31 @@ public void itCanBeDetachedFromTheEngineAndStopSendingFurtherEvents() {
verify(mockDelegate, times(1)).onDetach();
}
+ @Test
+ public void itDelaysDrawing() {
+ Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application);
+ ActivityController