APP 启动页在国内是最常见也是必备的场景,其中启动页在 iOS 上算是强制性的要求,其实配置启动页挺简略,因为在 Flutter 里当初只须要:
- iOS 配置
LaunchScreen.storyboard
; - Android 配置
windowBackground
;
个别只有配置无误并且图片尺寸匹配,基本上就不会有什么问题,那既然这样,还有什么须要适配的呢?
事实上大部分时候 iOS 是不会有什么问题,因为 LaunchScreen.storyboard
的流程本就是 iOS 官网用来做利用启动的过渡;而对于 Andorid 而言,直到 12 之前 windowBackground
这种其实只能算“民间”野路子,所以对于 Andorid 来说,这其中就波及到一个点:
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"android:value="true" />
所以上面次要介绍 Flutter 在 Android 上为了这个启动图做了哪些骚操作~
一、远古期间
在曾经遗记版本的“远古期间”, FlutterActivity
还在 io.flutter.app.FlutterActivity
门路下的时候,那时启动页的逻辑绝对简略,次要是通过 App 的 AndroidManifest
文件里是否配置了 SplashScreenUntilFirstFrame
来进行判断。
<meta-data android:name="io.flutter.app.android.SplashScreenUntilFirstFrame" android:value="true" />
在 FlutterActivity
外部 FlutterView
被创立的时候,会通过读取 meta-data
来判断是否须要应用 createLaunchView
逻辑:
- 1、获取以后主题的
android.R.attr.windowBackground
这个Drawable
; - 2、创立一个
LaunchView
并加载这个Drawable
; - 3、将这个
LaunchView
增加到Activity
的ContentView
; - 4、在Flutter
onFirstFrame
时将这个LaunchView
移除;
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); } }
是不是很简略,那就会有人疑难为什么要这样做?我间接配置 Activity
的 android:windowBackground
不就实现了吗?
这就是下面提到的时间差问题,因为启动页到 Flutter 渲染完第一帧画面两头,会呈现概率呈现黑屏的状况,所以才须要这个行为来实现过渡。
2.5 之前
经验了“远古时代”之后,FlutterActivity
来到了 io.flutter.embedding.android.FlutterActivity
, 在到 2.5 版本公布之前,Flutter 又针对这个启动过程做了不少调整和优化,其中次要就是 SplashScreen
。
自从开始进入embedding
阶段后,FlutterActivity
次要用于实现了一个叫 Host
的 interface
,其中和咱们有关系的就是 provideSplashScreen
。
默认状况下它会从 AndroidManifest
文件里是否配置了 SplashScreenDrawable
来进行判断。
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" />
默认状况下当 AndroidManifest 文件里配置了 SplashScreenDrawable
,那么这个 Drawable 就会在 FlutterActivity
创立 FlutterView
时被构建成 DrawableSplashScreen
。
DrawableSplashScreen
其实就是一个实现了 io.flutter.embedding.android.SplashScreen
接口的类,它的作用就是:
在 Activity 创立 FlutterView 的时候,将AndroidManifest
里配置的SplashScreenDrawable
加载成splashScreenView
(ImageView);,并提供transitionToFlutter
办法用于执行。
之后 FlutterActivity
内会创立出 FlutterSplashView
,它是个 FrameLayout。
FlutterSplashView
将 FlutterView
和 ImageView
增加到一起, 而后通过 transitionToFlutter
的办法来执行动画,最初动画完结时通过 onTransitionComplete
移除 splashScreenView
。
所以整体逻辑就是:
- 依据 meta 创立
DrawableSplashScreen
; FlutterSplashView
先增加了FlutterView
;FlutterSplashView
先增加了splashScreenView
这个 ImageView;- 最初在
addOnFirstFrameRenderedListener
回调里执行transitionToFlutter
去触发 animate ,并且移除splashScreenView
。
当然这里也是分状态:
- 等引擎加载实现之后再执行
transitionToFlutter
; - 引擎曾经加载实现了马上执行
transitionToFlutter
; - 以后的
FlutterView
还没有被增加到引擎,期待增加到引擎之后再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(); }
当然这个阶段的 FlutterActivity 也能够通过 override
provideSplashScreen
办法来自定义 SplashScreen。
留神这里的 SplashScreen 不等于 Android 12 的 SplashScreen。
看到没有,做了这么多其实也就是为了补救启动页和 Flutter 渲染之间,另外还有一个优化,叫 NormalTheme
。
当咱们设置了一个Activity
的windowBackground
之后,其实对性能还是多多少少会有影响,所以官网就减少了一个NormalTheme
的配置,在启动实现之后将主题设置为开发者本人配置的NormalTheme
。
通过该配置 NormalTheme
,在 Activity
启动时,就会首先执行 switchLaunchThemeForNormalTheme();
办法将主题从 LaunchTheme
切换到 NormalTheme
。
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
大略配置完就是如下样子,后面剖析那么多其实就是为了通知你,如果呈现问题了,你能够从哪个中央去找到对应的点。
<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>
2.5 之后
讲了那么多,Flutter 2.5 之后 provideSplashScreen
和 io.flutter.embedding.android.SplashScreenDrawable
就被弃用了,惊不喜惊喜,意不意外,开不开心 ?
Flutter 官网说: Flutter 当初会主动维持着 Android 启动页面的效显示,直到 Flutter 绘制完第一帧后才隐没。
通过源码你会发现,当你设置了 splashScreen
的时候,会看到一个 log 正告:
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; }
为什么会弃用? 其实这个提议是在 github.com/flutter/flu… 这个 issue 上,而后通过 github.com/flutter/eng… 这个 pr 实现调整。
大略意思就是:本来的设计搞简单了,用 OnPreDrawListener
更精准,而且不须要为了前面 Andorid12 的启动反对做其余兼容,只须要给 FlutterActivity 等类减少接口开关即可。
也就是2.5之后 Flutter 应用 ViewTreeObserver.OnPreDrawListener 来实现提早直到加载出 Flutter 的第一帧。
为什么说默认状况?因为这个行为在 FlutterActivity 里,是在 getRenderMode() == RenderMode.surface
才会被调用,而 RenderMode
又和 BackgroundMode
有关怀。
默认状况下 BackgroundMode 就是BackgroundMode.opaque
,所以就是RenderMode.surface
所以在 2.5 版本后, FlutterActivity 外部创立完 FlutterView 后就会执行一个 delayFirstAndroidViewDraw
的操作。
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); }}
这里次要留神一个参数:isFlutterUiDisplayed
。
当 Flutter 被实现展现的时候,isFlutterUiDisplayed
就会被设置为 true。
所以当 Flutter 没有执行实现之前,FlutterView
的 onPreDraw
就会始终返回 false,这也是 Flutter 2.5 开始之后适配启动页的新调整。
最初
看了这么多,大略能够看到其实开源我的项目的推动并不是一帆风顺的,没有什么是一开始就是最优解,而是通过多方尝试和交换,才有了当初的版本,事实上开源我的项目里,相似这样的经验不可胜数:
#### 相干视频举荐:
【2021最新版】Android studio装置教程+Android(安卓)零基础教程视频(适宜Android 0根底,Android初学入门)含音视频_哔哩哔哩_bilibili
【 Android进阶教程】——Framework面试必问的Handler源码解析_哔哩哔哩_bilibili
Android进阶零碎学习——Gradle入门与我的项目实战_哔哩哔哩_bilibili
Android架构设计原理与实战——Jetpack联合MVP组合利用开发一个优良的APP!_哔哩哔哩_bilibili
本文转自 https://juejin.cn/post/7038516159318065165,如有侵权,请分割删除。