The APP startup page is the most common and necessary scenario in China. The startup page is a mandatory requirement on iOS. In fact, configuring the startup page is very simple, because in fluent, you only need:
- Configure LaunchScreen.storyboard for iOS;
- Configure windows background for Android;
Generally, as long as the configuration is correct and the picture size matches, there will be no problem. In that case, what else needs to be adapted?
In fact, there will be no problem with iOS most of the time, because the process of LaunchScreen.storyboard is the official transition of iOS for application startup; As far as Andorid is concerned, until 12 years ago, the window background was only a "folk" wild way, so as far as Andorid is concerned, this involves one point:
[Flutter's first frame] + [time needed to jump from raster to main thread and get a next Android vsync] = [Android's first frame].
Therefore, the following mainly introduces what operations Flutter has done for this startup diagram on Android ~
1, Ancient times
In the "ancient times" of the forgotten version, when the fluteractivity was still under the io.flutter.App.fluteractivity path, the logic of the startup page was relatively simple at that time, which was mainly determined by whether the SplashScreenUntilFirstFrame was configured in the App's AndroidManifest file.
<meta-data android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:value="true" /> Copy code
When the FlutterView inside the FlutterActivity is created, it will judge whether to use the createLaunchView logic by reading meta data:
- 1. Get the android.r.attr.windowsbackground Drawable of the current topic;
- 2. Create a LaunchView and load the Drawable;
- 3. Add the LaunchView to the ContentView of the Activity;
- 4. Remove the LaunchView when the shutter is on the firstframe;
private void addLaunchView() { if (this.launchView != null) { this.activity.addContentView(this.launchView, matchParent); this.flutterView.addFirstFrameListener(new FirstFrameListener() { public void onFirstFrame() { FlutterActivityDelegate.this.launchView.animate().alpha(0.0F).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 code
Is it very simple, then some people will wonder why they do this? Don't I just configure the android:windowBackground of the Activity?
This is the problem of time difference mentioned above, because there is a probability that a black screen will appear between the start page and the first frame rendered by fluent, so this behavior is needed to realize the transition.
Before 2.5
After "ancient times", FlutterActivity came to io.flutter.embedding.android.FlutterActivity. Before the release of version 2.5, Flutter made many adjustments and optimizations for this startup process, mainly SplashScreen.
Since entering the embedding stage, fluteractivity is mainly used to implement an interface called Host, of which providesplayscreen is related to us.
By default, it will judge whether SplashScreenDrawable is configured in the AndroidManifest file.
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" /> Copy code
By default, when SplashScreenDrawable is configured in the AndroidManifest file, this Drawable will be built as DrawableSplashScreen when FlutterActivity creates a FlutterView.
DrawableSplashScreen is actually a class that implements the io.fluent.embedding.android.splashscreen interface. Its function is:
When the Activity creates the fluterview, load the SplashScreenDrawable configured in the AndroidManifest into splashScreenView(ImageView);, The transitionToFlutter method is provided for execution.
After that, the FlutterSplashView will be created in the FlutterActivity, which is a FrameLayout.
FlutterSplashView adds FlutterView and ImageView together, and then executes the animation through the method of transitionToFlutter. Finally, at the end of the animation, remove the splashScreenView through onTransitionComplete.
So the overall logic is:
- Create DrawableSplashScreen according to meta;
- FlutterSplashView adds FlutterView first;
- FlutterSplashView first adds the ImageView of splashScreenView;
- Finally, execute transitionToFlutter in the addOnFirstFrameRenderedListener callback to trigger the animate and remove the splashScreenView.
Of course, this is also a sub state:
- Execute transitionToFlutter after the engine is loaded;
- After the engine has been loaded, execute transition toflutter immediately;
- The current fluterview has not been added to the engine. Wait until it is added to the engine before transitionToFlutter;
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() { return this.flutterView != null && this.flutterView.isAttachedToFlutterEngine() && !this.flutterView.hasRenderedFirstFrame() && !this.hasSplashCompleted(); } private boolean isSplashScreenTransitionNeededNow() { return this.flutterView != null && this.flutterView.isAttachedToFlutterEngine() && this.splashScreen != null && this.splashScreen.doesSplashViewRememberItsTransition() && this.wasPreviousSplashTransitionInterrupted(); } Copy code
Of course, the fluteractivity at this stage can also customize the SplashScreen through the override provideSplashScreen method.
Note that the SplashScreen here is not equal to the SplashScreen of Android 12.
See, we have done so much to make up for the gap between the startup page and fluent rendering. There is also an optimization called NormalTheme.
When we set an Activity's windowBackground, it will actually have an impact on the performance. Therefore, the official added a NormalTheme configuration. After startup, set the theme to the NormalTheme configured by the developer.
By configuring NormalTheme, switchLaunchThemeForNormalTheme() will be executed first when the Activity is started; Method to switch the theme from LaunchTheme to NormalTheme.
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> Copy code
After the configuration, it looks like the following. 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 code
After 2.5
After talking so much, providesplayscreen and io.fluent.embedding.android.splashscreendrawable were abandoned after fluent 2.5. Are you surprised, surprised and happy?
Flutter official said: flutter will now automatically maintain the effective display of Android startup page until flutter finishes drawing the first frame.
Through the source code, you will find that 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 code
Why is it abandoned? In fact, this proposal is github.com/flutter/flu... On this issue, and then through github.com/flutter/eng... This pr completes the adjustment.
The original design is complicated. It is more accurate to use OnPreDrawListener, and there is no need to make other compatibility for the later Andorid12 startup support. It only needs to add interface switches to classes such as fluteractivity.
That is, after 2.5, the Flutter is used ViewTreeObserver.OnPreDrawListener To delay until the first frame of the Flutter is loaded.
Why default? Because this behavior is called in the FlutterActivity only when getRenderMode() == RenderMode.surface, and RenderMode is concerned with BackgroundMode.
By default, BackgroundMode is BackgroundMode.opaque, so it is RenderMode.surface
Therefore, after version 2.5, a delayFirstAndroidViewDraw operation will be executed after the FlutterView is created inside the FlutterActivity.
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() { 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 code
Here, we mainly pay attention to one parameter: isFlutterUiDisplayed.
isFlutterUiDisplayed will be set to true when the display of Flutter is completed.
Therefore, the onPreDraw of FlutterView will always return false before the execution of Flutter is completed, which is also a new adjustment of the adaptation startup page after the start of Flutter 2.5.
last
After reading so much, we can probably see that the promotion of open source projects is not plain sailing. There is nothing that is the optimal solution at the beginning, but the current version is obtained after many attempts and exchanges. In fact, there are countless experiences like this in open source projects: