Flutter 啟動頁(閃屏頁)具體實現和原理分析

caobugs發表於2019-04-12

為什麼要有啟動頁?

在以下文章中,啟動頁就是閃屏頁。

現在大部分App都有啟動頁,那麼為什麼要有啟動頁?這是個值得思考的問題,如果沒有啟動頁會怎樣,大部分的App會白屏(也有可能是黑屏,主題設定有關係)非常短的時間,然後才能展示App的內容。

那麼問題來了,一定要有啟動頁嗎?答案:不是,而且是儘可能不要有啟動頁,因為啟動頁會讓使用者體驗不夠連貫,甚至IOS在開發手冊上就不推薦使用啟動頁。

我們深入思考一下,既然不推薦為什麼這樣流行,答案非常簡單,啟動頁的成本非常低,如果你想把的App啟動優化到一個非常短的時間,還是有一定成本的。

Android啟動流程

為什麼要談Android的啟動流程呢?因為Flutter啟動的時候,依賴的是Android的執行環境,其本質是Activity上新增了一個FlutterView,FlutterView繼承SurfaceView,那麼就容易理解了,Flutter的全部頁面都是渲染到了FlutterView上,如果不熟悉Flutter的啟動流程可以參考我寫的Flutter啟動流程 這篇文章,下面是對Flutter啟動的一個簡單描述。

Flutter 啟動頁(閃屏頁)具體實現和原理分析

在Flutter中,啟動頁的作用是在FlutterView顯示第一幀之前,不要出現白屏,在FlutterView顯示第一幀之前,我們分成兩個階段,Android啟動階段和Flutter啟動階段,Android啟過程新增啟動頁非常容易,在主題xml中新增android:windowBackground屬性,Flutter怎麼新增啟動頁呢?其實框架已經幫助我們們實現好了,我下面就給大家說一下原理。

Flutter啟動頁具體實現和原理

  1. 建立一個SplashActivity,這Activity繼承FlutterActivity,重寫onCreate()方法,在onCreate()方法中呼叫GeneratedPluginRegistrant.registerWith(),下面是啟動頁的程式碼。

    public class SplashActivity extends FlutterActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            GeneratedPluginRegistrant.registerWith(this);
        }
    }
    複製程式碼
  2. 在Manifest中新增SplashActivity作為App的啟動Activity,設定SplashActivity的主題是LaunchTheme。下面是Manifest的配置檔案。

     <activity
                android:name=".SplashActivity"
         android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:hardwareAccelerated="true"
                android:launchMode="singleTop"
                android:theme="@style/LaunchTheme"
                android:windowSoftInputMode="adjustResize">
                <meta-data
                    android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                    android:value="true" />
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    複製程式碼
  3. meta-data的name = "io.flutter.app.android.SplashScreenUntilFirstFrame"的value一定要設定成true,一定要設定成true,一定要設定成true重要的事情說三遍,如果這個屬性設定成false,效果是這樣的。

    Flutter 啟動頁(閃屏頁)具體實現和原理分析

    從現象觀察,啟動頁中間有一段時間黑屏,這個為什麼呢?前面我們說過,Flutter的啟動流程分成兩部分,一部分是Android啟動階段,一個是Flutter的啟動階段,這個黑屏就是Flutter的啟動階段沒有啟動頁所造成的。我們從原始碼入手,詳細分析一下,下面是FlutterActivityDelegate的部分原始碼。

    public final class FlutterActivityDelegate
            implements FlutterActivityEvents,
                       FlutterView.Provider,
                       PluginRegistry {
        private static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.app.android.SplashScreenUntilFirstFrame";
      
        private View launchView;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            String[] args = getArgsFromIntent(activity.getIntent());
            FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), args);
            flutterView = viewFactory.createFlutterView(activity);
            if (flutterView == null) {
                FlutterNativeView nativeView = viewFactory.createFlutterNativeView();
                flutterView = new FlutterView(activity, null, nativeView);
                flutterView.setLayoutParams(matchParent);
                activity.setContentView(flutterView);
                launchView = createLaunchView();//1
                if (launchView != null) {
                    addLaunchView();//2
                }
            }
        }
                   
        private View createLaunchView() {
              if (!showSplashScreenUntilFirstFrame()) {//3
                  return null;
              }
              final Drawable launchScreenDrawable = getLaunchScreenDrawableFromActivityTheme();
              final View view = new View(activity);
              view.setBackground(launchScreenDrawable);
              return view;
          }
                         
        private Drawable getLaunchScreenDrawableFromActivityTheme() {
            //省略了部分程式碼
            try {
                return activity.getResources().getDrawable(typedValue.resourceId);
            } catch (NotFoundException e) {
                return null;
            }
        }
        
       private Boolean showSplashScreenUntilFirstFrame() {
            try {
                ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo(
                    activity.getComponentName(),
                    PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES);
                Bundle metadata = activityInfo.metaData;
                return metadata != null && metadata.getBoolean(SPLASH_SCREEN_META_DATA_KEY);
            } catch (NameNotFoundException e) {
                return false;
            }
        }
                         
        private void addLaunchView() {
            activity.addContentView(launchView, matchParent);//4
            flutterView.addFirstFrameListener(new FlutterView.FirstFrameListener() {//5
                @Override
                public void onFirstFrame() {
                    FlutterActivityDelegate.this.launchView.animate()
                        .alpha(0f)
                        .setListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                ((ViewGroup) FlutterActivityDelegate.this.launchView.getParent())
                                    .removeView(FlutterActivityDelegate.this.launchView);//5
                            }
                        });
                }
            });
            activity.setTheme(android.R.style.Theme_Black_NoTitleBar);
        }
    }
    複製程式碼
  • 註釋1

    這個段程式碼很容易理解,建立一個LaunchView,主要邏輯在createLaunchView()中,原理也很簡單,根據主題中的R.attr.windowBackground屬性,生成一個Drawable,然後建立了一個View,並且把這個View的背景設定成Drawable。

  • 註釋3

    showSplashScreenUntilFirstFrame()是得到Manifet中io.flutter.app.android.SplashScreenUntilFirstFrame的屬性的值,如果是false,那麼久返回一個空的的LaunchView,也就不會執行註釋2的程式碼。這就是我們上面說的如果設定成false就顯示黑屏的原因。

  • 註釋2

    呼叫addLaunchView(),這方法也很簡單,首先看註釋4,把LaunchView新增到當前的Activity中,然後新增了一個監聽,在註釋5處,這個監聽是當FlutterView第一幀載入完成後回撥,回撥做了什麼事情呢?很簡單,把LaunchView刪除了,顯示FlutterView的第一幀。

總結一下,就是把Android的啟動頁生成一個Drawable,建立了一個LaunchView,把Drawable設定成LaunchView的背景,當前的Activity新增這LaunchView,如果FlutterView的第一幀顯示了,把LaunchView刪除。

  1. 設定主題,下面是LaunchTheme的程式碼。

    <resources>
        <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
            <!-- Show a splash screen on the activity. Automatically removed when
                 Flutter draws its first frame -->
            <item name="android:windowBackground">@drawable/launch_background</item>
        </style>
    </resources>
    複製程式碼

    下面是launch_background的程式碼。

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:opacity="opaque">
        <item>
            <bitmap android:src="@mipmap/ic_launch_bg" />
        </item>
        <item
            android:width="90dp"
            android:height="90dp"
            android:gravity="center">
            <bitmap android:src="@mipmap/ic_launch_logo" />
        </item>
    </layer-list>
    複製程式碼

最終效果如下,沒有黑屏,非常順滑。

Flutter 啟動頁(閃屏頁)具體實現和原理分析

參考

相關文章