关于android:Flutter-启动页的前世今生适配历程

2次阅读

共计 8051 个字符,预计需要花费 21 分钟才能阅读完成。

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 增加到 ActivityContentView
  • 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);
       }
   }

是不是很简略,那就会有人疑难为什么要这样做?我间接配置 Activityandroid:windowBackground 不就实现了吗?

这就是下面提到的时间差问题, 因为启动页到 Flutter 渲染完第一帧画面两头,会呈现概率呈现黑屏的状况,所以才须要这个行为来实现过渡

2.5 之前

经验了“远古时代”之后,FlutterActivity 来到了 io.flutter.embedding.android.FlutterActivity,在到 2.5 版本公布之前,Flutter 又针对这个启动过程做了不少调整和优化,其中次要就是 SplashScreen

自从开始进入 embedding 阶段后,FlutterActivity 次要用于实现了一个叫 Hostinterface,其中和咱们有关系的就是 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。

FlutterSplashViewFlutterViewImageView 增加到一起,而后通过 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

当咱们设置了一个 ActivitywindowBackground 之后,其实对性能还是多多少少会有影响,所以官网就减少了一个 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 之后 provideSplashScreenio.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 没有执行实现之前,FlutterViewonPreDraw 就会始终返回 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,如有侵权,请分割删除。

正文完
 0