在以前的 《Android PlatformView 和鍵盤問題》 一文中介紹過混合開發(fā)上 Android PlatformView
的實現(xiàn)和問題,原本 Android 平臺上為了集成如 WebView
、MapView
等能力,使用了 VirtualDisplays
的實現(xiàn)方式。
如今 1.20 官方開始嘗試推出和 iOS PlatformView
類似的新 Hybrid Composition
模式,本篇將通過三小節(jié)對比介紹 Hybrid Composition
的使用和原理,一起來吃“螃蟹”吧~
反復(fù)提醒,是 1.20 不是 1.2 ~~~
一、舊版本的 VirtualDisplay
1.20 之前在 Flutter 中通過將 AndroidView
需要渲染的內(nèi)容繪制到 VirtualDisplays
中
,然后在 VirtualDisplay
對應(yīng)的內(nèi)存中,繪制的畫面就可以通過其 Surface
獲取得到。
VirtualDisplay
類似于一個虛擬顯示區(qū)域,需要結(jié)合DisplayManager
一起調(diào)用,一般在副屏顯示或者錄屏場景下會用到。VirtualDisplay
會將虛擬顯示區(qū)域的內(nèi)容渲染在一個Surface
上。
如上圖所示,簡單來說就是原生控件的內(nèi)容被繪制到內(nèi)存里,然后 Flutter Engine 通過相對應(yīng)的 textureId
就可以獲取到控件的渲染數(shù)據(jù)并顯示出來。
這種實現(xiàn)方式最大的問題就在與觸摸事件、文字輸入和鍵盤焦點(diǎn)等方面存在很多諸多需要處理的問題;在 iOS 并不使用類似 VirtualDisplay
的方法,而是通過將 Flutter UI 分為兩個透明紋理來完成組合:一個在 iOS 平臺視圖之下,一個在其上面。
所以這樣的好處就是:需要在“iOS平臺”視圖下方呈現(xiàn)的Flutter UI,最終會被繪制到其下方的紋理上;而需要在“平臺”上方呈現(xiàn)的Flutter UI,最終會被繪制在其上方的紋理。它們只需要在最后組合起來就可以了。
通常這種方法更好,因為這意味著 Native View 可以直接參與到 Flutter 的 UI 層次結(jié)構(gòu)中。
二、 接入 Hybrid Composition
官方和社區(qū)不懈的努力下, 1.20 版本開始在 Android 上新增了 Hybrid Composition
的 PlatformView
實現(xiàn),該實現(xiàn)將解決以前存在于 Android 上的大部分和 PlatformView
相關(guān)的問題,比如華為手機(jī)上鍵盤彈出后 Web 界面離奇消失等玄學(xué)異常。
使用 Hybrid Composition
需要使用到 PlatformViewLink、 AndroidViewSurface 和 PlatformViewsService 這三個對象,首先我們要創(chuàng)建一個 dart 控件:
- 通過
PlatformViewLink
的viewType
注冊了一個和原生層對應(yīng)的注冊名稱,這和之前的PlatformView
注冊一樣; - 然后在
surfaceFactory
返回一個AndroidViewSurface
用于處理繪制和接受觸摸事件; - 最后在
onCreatePlatformView
方法使用PlatformViewsService
初始化AndroidViewSurface
和初始化所需要的參數(shù),同時通過 Engine 去觸發(fā)原生層的顯示。
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
final String viewType = 'hybrid-view-type';
// Pass parameters to the platform side.
final Map<String, dynamic> creationParams = <String, dynamic>{};
return PlatformViewLink(
viewType: viewType,
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
接下來來到 Android 原生層,在原生通過繼承 PlatformView
然后通過 getView
方法返回需要渲染的控件。
package dev.flutter.example;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.platform.PlatformView;
class NativeView implements PlatformView {
@NonNull private final TextView textView;
NativeView(@NonNull Context context, int id, @Nullable Map<String, Object> creationParams) {
textView = new TextView(context);
textView.setTextSize(72);
textView.setBackgroundColor(Color.rgb(255, 255, 255));
textView.setText("Rendered on a native Android view (id: " + id + ")");
}
@NonNull
@Override
public View getView() {
return textView;
}
@Override
public void dispose() {}
}
之后再繼承 PlatformViewFactory
通過 create
方法來加載和初始化 PlatformView
。
package dev.flutter.example;
import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;
class NativeViewFactory extends PlatformViewFactory {
@NonNull private final BinaryMessenger messenger;
@NonNull private final View containerView;
NativeViewFactory(@NonNull BinaryMessenger messenger, @NonNull View containerView) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
this.containerView = containerView;
}
@NonNull
@Override
public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
final Map<String, Object> creationParams = (Map<String, Object>) args;
return new NativeView(context, id, creationParams);
}
}
最后在 MainActivity
通過 flutterEngine
的 getPlatformViewsController
去注冊 NativeViewFactory
。
package dev.flutter.example;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory("hybrid-view-type", new NativeViewFactory(null, null));
}
}
當(dāng)然,如果需要在 Android 上啟用 Hybrid Composition
,還需要在 AndroidManifest.xml
添加如下所示代碼來啟用配置:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.flutter.example">
<application
android:name="io.flutter.app.FlutterApplication"
android:label="hybrid"
android:icon="@mipmap/ic_launcher">
<!-- ... -->
<!-- Hybrid composition -->
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
</application>
</manifest>
另外,官方表示 Hybrid composition
在 Android 10 以上的性能表現(xiàn)不錯,在 10 以下的版本中,F(xiàn)lutter 界面在屏幕上呈現(xiàn)的速度會變慢,這個開銷是因為 Flutter 幀需要與 Android 視圖系統(tǒng)同步造成的。
為了緩解此問題,應(yīng)該避免在 Dart 執(zhí)行動畫時顯示原生控件,例如可以使用placeholder 來原生控件的屏幕截圖,并在這些動畫發(fā)生時直接使用這個 placeholder。
三、 Hybrid Composition 的特點(diǎn)和實現(xiàn)原理
要介紹 Hybrid Composition
的實現(xiàn),就不得不介紹本次新增的一個對象:FlutterImageView
。
FlutterImageView
并不是一般意義上的ImageView
。
事實上 Hybrid Composition
上混合原生控件所需的圖層合成就是通過 FlutterImageView
來實現(xiàn)。FlutterImageView
本身是一個普通的原生 View
, 它通過實現(xiàn)了 RenderSurface
接口從而實現(xiàn)如 FlutterSurfaceView
的部分能力。
在 FlutterImageView
內(nèi)部主要有 ImageReader
、Image
和 Bitmap
三種類,其中:
-
ImageReader
可以簡單理解為就是能夠存儲Image
數(shù)據(jù)的對象,并且可以提供Surface
用于繪制接受原生層的Image
數(shù)據(jù)。 -
Image
就是包含了ByteBuffers
的像素數(shù)據(jù),它和ImageReader
一般用在原生的如Camera
相關(guān)的領(lǐng)域。 -
Bitmap
是將Image
轉(zhuǎn)化為可以繪制的位圖,然后在FlutterImageView
內(nèi)通過Canvas
繪制出來。
可以看到 FlutterImageView
可以提供 Surface
,可以讀取到 Surface
的 Image
數(shù)據(jù),然后通過Bitmap
繪制出來。
而在 FlutterImageView
中提供有 background
和 overlay
兩種 SurfaceKind
,其中:
background
適用于默認(rèn)下FlutterView
的渲染模式,也就是 Flutter 主應(yīng)用的渲染默認(rèn),所以FlutterView
其實現(xiàn)在有surface
、texture
和image
三種RenderMode
。overlay
就是用于上面所說的Hybrid Composition
下用于和PlatformView
合成的模式。
另外還有一點(diǎn)可以看到,在 PlatformViewsController
里有 createAndroidViewForPlatformView
和 createVirtualDisplayForPlatformView
兩個方法,這也是 Flutter 官方在提供 Hybrid Composition
的同時也兼容 VirtualDisplay
默認(rèn)的一種做法。
Hybrid Composition
Dart 層通過PlatformViewsService
觸發(fā)原生的PlatformViewsChannel
的create
方法,之后發(fā)起一個PlatformViewCreationRequest
就會有usesHybridComposition
的判斷,如果為 ture 后面就是走的createAndroidViewForPlatformView
。
那么 Hybrid Composition
模式下 FlutterImageView
是如何工作的呢?
首先我們把上面第二小節(jié)的例子跑起來,同時打開 Android 手機(jī)的布局邊界,可以看到屏幕中間出現(xiàn)了一個包含 Re
的白色小方塊。通過布局邊界可以看到, Re
白色小方塊其實是一個原生控件。
接著用同樣的代碼在不同位置增加一個 Re
白色小方塊,可以看到屏幕的右上角又多了一個有布局邊界的 Re
白色小方塊,所以可以看到 Hybrid Composition
模式下的 PlatformView
是通過某種原生控件顯示出來的。
但是我們就會想了,在 Flutter
上放原生控件有什么稀奇的?這就算是圖層合成了?那么接著把兩個 Re
白色小方塊放到一起,然后在它們上面不用 PlatformView
而是直接用默認(rèn)的 Text
繪制一個藍(lán)色的 Re
文本。
看到?jīng)]有?在不用 PlatformView
的情況下,Text
繪制的藍(lán)色的 Re
文本居然可以顯示在白色不透明的原生 Re
白色小方塊上!!!
也許有的小伙伴會說,這有什么稀奇的?但是知道
Flutter
首先原理的應(yīng)該知道,Flutter
在原生層默認(rèn)情況下就是一個SurfaceView
,然后 Engine 把所有畫面控件渲染到這個Surface
上。但是現(xiàn)在你看到了什么?我們在 Dart 層的
Text
藍(lán)色的Re
文本居然可以現(xiàn)在到Re
白色小方塊上,這說明Hybrid Composition
不僅僅是把原生控件放到 Flutter 上那么簡單。
然后我們又發(fā)現(xiàn)了另外一個奇怪的問題,用 Flutter 默認(rèn) Text
繪制的藍(lán)色的 Re
文本居然也有原生的布局邊界顯示?所以我們又用默認(rèn) Text
增加了黃色的 Re
文本和紅色的 Re
文本 ,可以看到只有和 PlatformView
有交集的 Text
出現(xiàn)了布局邊界。
接著將黃色的 Re
文本往下調(diào)整后,可以看到黃色 Re
文本的布局邊界也消失了,所以可以判定 Hybrid Composition
下 Dart 控件之所以可以顯示在原生控件之上,是因為在和 PlatformView
有交集時通過某種原生控件重新繪制。
所以我們通過 Layout Inspector
可以看到,重疊的 Text
控件是通過 FlutterImageView
層來實現(xiàn)渲染。
另外還有一個有趣的現(xiàn)象,那就是當(dāng) Flutter 有不只一個默認(rèn)的控件本被顯示在一個 PlatformView
區(qū)域上時,那么這幾個控件會共用一個 FlutterImageView
。
而如果他們不在一個區(qū)域內(nèi),那么就會各自使用自己的 FlutterImageView
。另外可以注意到,用 Hybrid Composition
默認(rèn)接入的 PlatformView
是一個 FlutterMutatorView
。
其實 FlutterMutatorView
是用于調(diào)整原生控件接入到 FlutterView
的位置和 Matrix
的,一般情況下 Hybrid Composition
下的 PlatformView
接入關(guān)系是:
所以 PlatformView
是通過 FlutterMutatorView
把原生控件 addView
到 FlutterView
上,然后再通過 FlutterImageView
的能力去實現(xiàn)圖層的混合。
那么 Flutter 是怎么判斷控件需要使用 FlutterImageView
?
事實上可以看到,在 Engine 去 SubmitFrame
時,會通過 current_frame_view_count
去對每個 view 畫面進(jìn)行規(guī)劃處理,然后會通過判定區(qū)域內(nèi)是否需要 CreateSurfaceIfNeeded
函數(shù),最終觸發(fā)原生的 createOverlaySurface
方法去創(chuàng)建 FlutterImageView
。
for (const SkRect& overlay_rect : overlay_layers.at(view_id)) {
std::unique_ptr<SurfaceFrame> frame =
CreateSurfaceIfNeeded(context, //
view_id, //
pictures.at(view_id), //
overlay_rect //
);
if (should_submit_current_frame) {
frame->Submit();
}
}
至于在 Dart 層面 PlatformViewSurface
就是通過 PlatformViewRenderBox
去添加 PlatformViewLayer
,然后再通過在 ui.SceneBuilder
的 addPlatformView
調(diào)用 Engine 添加 Layer
信息。(這部分內(nèi)容可見 《 Flutter 畫面渲染的全面解析》)
其實還有很多的實現(xiàn)細(xì)節(jié)沒介紹,比如:
-
onDisplayPlatformView
方法,也就是在展示PlatformView
時,會調(diào)用flutterView.convertToImageView
方法將renderSurface
切換為flutterImageView
; - 在
initializePlatformViewIfNeeded
方法里初始化過的PlatformViews
不會再次初始化創(chuàng)建; -
FlutterImagaeView
在createImageReader
和updateCurrentBitmap
時, Android 10 上可以通過 GPU 實現(xiàn)硬件加速,這也是為什么Hybrid Composition
在 Android 10 上性能較好的原因。
因為篇(tou)幅(lan)剩下就不一一展開了,目前 Hybrid Composition
已經(jīng)在 1.20 stable 版本上可用了,也解決了我在鍵盤上的一些問題,當(dāng)然 Hybrid Composition 能否經(jīng)受住考驗?zāi)侵荒茏寱r間決定了,畢竟一步一個坑不是么~
資源推薦
- Github : https://github.com/CarGuo
- 開源 Flutter 完整項目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學(xué)習(xí)型項目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實戰(zhàn)電子書項目:https://github.com/CarGuo/GSYFlutterBook