在以前的 《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)方式最大的問題就在與觸摸事件填抬、文字輸入和鍵盤焦點等方面存在很多諸多需要處理的問題;在 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)的問題芥喇,比如華為手機上鍵盤彈出后 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 的特點和實現(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
合成的模式执赡。
另外還有一點可以看到镰踏,在 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 手機的布局邊界猜拾,可以看到屏幕中間出現(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