The APP launch page is the most common and necessary scene in China. The launch page is a mandatory requirement on iOS. In fact, configuring the launch page is quite simple, because Flutter now only needs to:

  • IOS configurationLaunchScreen.storyboard;
  • Android configurationwindowBackground;

Generally, as long as the configuration is correct and the size of the picture matches, basically there will be no problem. Since this is the case, what else needs to be adapted?

In fact, most of the time iOS doesn’t have a problem with launchscreen.storyboard because the launchscreen.storyboard process is the official iOS app launch transition; For Android, Windows Background is a “folk” way until 12, so for Android, there is a point involved:

[Flutter’s first frame] + [time needed to jump from raster to main thread and get a next Android vsync] = [Android’s first frame].

So here’s what Flutter does on Android to create this startup image

1. Ancient times

In have forgotten versions of “ancient times”, FlutterActivity in IO. Flutter. App. FlutterActivity path, then start page logic is relatively simple, Mainly through the App AndroidManifest file is configured with SplashScreenUntilFirstFrame for judgment.

 <meta-data
    android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
    android:value="true" />
Copy the code

When the FlutterView inside the FlutterActivity is created, the meta-data is read to determine whether the createLaunchView logic is needed:

  • 1, for the current theme android. State Richard armitage TTR event. WindowBackground the Drawable;

  • 2. Create a LaunchView and load the Drawable.

  • 3. Add the LaunchView to the Activity’s ContentView;

  • 4. Remove the LaunchView from Flutter onFirstFrame.

    private void addLaunchView(a) {
       if (this.launchView ! =null) {
           this.activity.addContentView(this.launchView, matchParent);
           this.flutterView.addFirstFrameListener(new FirstFrameListener() {
               public void onFirstFrame(a) {
                   FlutterActivityDelegate.this.launchView.animate().alpha(0.0 F).setListener(new AnimatorListenerAdapter() {
                       public void onAnimationEnd(Animator animation) {
                           ((ViewGroup)FlutterActivityDelegate.this.launchView.getParent()).removeView(FlutterActivityDelegate.this.launchView);
                           FlutterActivityDelegate.this.launchView = null; }}); FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this); }});this.activity.setTheme(16973833); }}Copy the code

Is it so simple that one wonders why? I’ll configure the Activity’s Android :windowBackground directly.

This is the time difference problem mentioned above, because there is a probability of a black screen between the start page and the end of the first frame of the Flutter rendering, so this behavior is needed to achieve the transition.

2.5 before

After the “ancient” FlutterActivity came to the IO. Flutter. The embedding. Android. FlutterActivity, before to version 2.5 release, flutter and done a lot of adjustment and optimization for the boot process, Chief among them is SplashScreen.

Since it enters the embedding stage, FlutterActivity is mainly used to implement an interface called Host, among which provideSplashScreen is relevant to us.

By default, it determines whether SplashScreenDrawable is configured in the AndroidManifest file.

 <meta-data
      android:name="io.flutter.embedding.android.SplashScreenDrawable"
      android:resource="@drawable/launch_background"
      />
Copy the code

By default when SplashScreenDrawable is configured in the AndroidManifest file, The Drawable is then constructed as the DrawableSplashScreen when the FlutterActivity creates the FlutterView.

DrawableSplashScreen is an implementation of a IO. Flutter. Embedding. Android. Interface SplashScreen class, its role is:

When the Activity creates the FlutterView, load the SplashScreenDrawable configured in the AndroidManifest as a splashScreenView(ImageView). And provides the transitionToFlutter method for execution.

The FlutterSplashView is then created within the FlutterActivity, which is a FrameLayout.

FlutterSplashView adds FlutterView and ImageView together, and then executes the animation transitionToFlutter, At the end of the animation, remove the splashScreenView with onTransitionComplete.

So the overall logic is:

  • Create DrawableSplashScreen according to meta;

  • FlutterSplashView added FlutterView first;

  • FlutterSplashView added splashScreenView ImageView;

  • Finally in the addOnFirstFrameRenderedListener callback execution transitionToFlutter to trigger the animate, and remove splashScreenView.

Of course, there are states:

  • Wait until the engine is loadedtransitionToFlutter;
  • The engine is loaded and ready to gotransitionToFlutter;
  • The currentFlutterViewHas not been added to the engine, waiting to be added to the enginetransitionToFlutter;
   public void displayFlutterViewWithSplash(@NonNull FlutterView flutterView, @Nullable SplashScreen splashScreen) {
       if (this.flutterView ! =null) {
           this.flutterView.removeOnFirstFrameRenderedListener(this.flutterUiDisplayListener);
           this.removeView(this.flutterView);
       }

       if (this.splashScreenView ! =null) {
           this.removeView(this.splashScreenView);
       }

       this.flutterView = flutterView;
       this.addView(flutterView);
       this.splashScreen = splashScreen;
       if(splashScreen ! =null) {
           if (this.isSplashScreenNeededNow()) {
               Log.v(TAG, "Showing splash screen UI.");
               this.splashScreenView = splashScreen.createSplashView(this.getContext(), this.splashScreenState);
               this.addView(this.splashScreenView);
               flutterView.addOnFirstFrameRenderedListener(this.flutterUiDisplayListener);
           } else if (this.isSplashScreenTransitionNeededNow()) {
               Log.v(TAG, "Showing an immediate splash transition to Flutter due to previously interrupted transition.");
               this.splashScreenView = splashScreen.createSplashView(this.getContext(), this.splashScreenState);
               this.addView(this.splashScreenView);
               this.transitionToFlutter();
           } else if(! flutterView.isAttachedToFlutterEngine()) { Log.v(TAG,"FlutterView is not yet attached to a FlutterEngine. Showing nothing until a FlutterEngine is attached.");
               flutterView.addFlutterEngineAttachmentListener(this.flutterEngineAttachmentListener); }}}private boolean isSplashScreenNeededNow(a) {
       return this.flutterView ! =null && this.flutterView.isAttachedToFlutterEngine() && !this.flutterView.hasRenderedFirstFrame() && !this.hasSplashCompleted();
   }

   private boolean isSplashScreenTransitionNeededNow(a) {
       return this.flutterView ! =null && this.flutterView.isAttachedToFlutterEngine() && this.splashScreen ! =null && this.splashScreen.doesSplashViewRememberItsTransition() && this.wasPreviousSplashTransitionInterrupted();
   }

Copy the code

Of course, the FlutterActivity of this stage can also customize the SplashScreen by overriding provideSplashScreen.

Note that SplashScreen here is not the same as Android 12’s SplashScreen.

See, all this is done to bridge the gap between the launch page and the Flutter rendering. There is also an optimization called the NormalTheme.

Setting an Activity’s windowBackground can affect performance, so we added a NormalTheme configuration. After launching, we set the theme to the NormalTheme configured by the developer.

In the Activity by the configuration NormalTheme started, will be executed first switchLaunchThemeForNormalTheme (); Method to switch the theme from LaunchTheme to NormalTheme.

    <meta-data
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/NormalTheme"
        />
       
Copy the code

The configuration is basically as follows. The previous analysis is actually to tell you where you can find the corresponding point if there is a problem.

<activity
    android:name=".MyActivity"
    android:theme="@style/LaunchTheme"
    // .
    >
    <meta-data
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/NormalTheme"
        />
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
Copy the code

After 2.5

Spoke so much, Flutter after 2.5 provideSplashScreen and IO Flutter. Embedding. The android. SplashScreenDrawable is deprecated, jing don’t like surprises, meaning is not accidental, open not happy?

According to Flutter officials, a Flutter now automatically maintains its display on the Android startup page until the Flutter has drawn its first frame.

When you set splashScreen, you will see a log warning:

    if(splashScreen ! =null) {
      Log.w(
          TAG,
          "A splash screen was provided to Flutter, but this is deprecated. See"
              + " flutter.dev/go/android-splash-migration for migration steps.");
      FlutterSplashView flutterSplashView = new FlutterSplashView(host.getContext());
      flutterSplashView.setId(ViewUtils.generateViewId(FLUTTER_SPLASH_VIEW_FALLBACK_ID));
      flutterSplashView.displayFlutterViewWithSplash(flutterView, splashScreen);

      return flutterSplashView;
    }
Copy the code

Why was it abandoned? The offer is actually made at github.com/flutter/flu… On this issue, and then via github.com/flutter/eng… This PR has been adjusted.

The OnPreDrawListener is more precise and doesn’t need to be compatible with later Andorid12 startup support. Just add interface switches for classes like FlutterActivity.

2.5 after Flutter using ViewTreeObserver. OnPreDrawListener to delay until loading the Flutter of the first frame.

Why the default? Surface is called only when getRenderMode() = RenderMode, and RenderMode and BackgroundMode are concerned.

By default BackgroundMode is Backgroundmode. opaque, so rendermode. surface

So, after the 2.5 version FlutterActivity internal after creating the FlutterView will perform a delayFirstAndroidViewDraw operations.


private void delayFirstAndroidViewDraw(final FlutterView flutterView) {
    if (this.host.getRenderMode() ! = RenderMode.surface) {throw new IllegalArgumentException("Cannot delay the first Android view draw when the render mode is not set to derMode.surface`.");
    } else {
        if (this.activePreDrawListener ! =null) {
            flutterView.getViewTreeObserver().removeOnPreDrawListener(this.activePreDrawListener);
        }

        this.activePreDrawListener = new OnPreDrawListener() {
            public boolean onPreDraw(a) {
                if (FlutterActivityAndFragmentDelegate.this.isFlutterUiDisplayed && terActivityAndFragmentDelegate.this.activePreDrawListener ! =null) {
                    flutterView.getViewTreeObserver().removeOnPreDrawListener(this);
                    FlutterActivityAndFragmentDelegate.this.activePreDrawListener = null;
                }

                return FlutterActivityAndFragmentDelegate.this.isFlutterUiDisplayed; }}; flutterView.getViewTreeObserver().addOnPreDrawListener(this.activePreDrawListener); }}Copy the code

There is only one parameter to note:isFlutterUiDisplayed.

When the Flutter is displayed, isFlutterUiDisplayed will be set to true.

So FlutterView’s onPreDraw will always return false until Flutter has completed execution. This is a new adjustment to the startup page since Flutter 2.5 started.

The last

After reading so much, we can probably see that the progress of open source projects is not always smooth. There is no optimal solution at the beginning, but the current version is achieved through multiple attempts and exchanges. In fact, there are numerous experiences like this in open source projects: