PlatformView 提供了在 Flutter 的 Widget 層級中嵌入原生視圖(iOS/Android等), PlatformView 在用來描述 iOS 平臺是視圖用的是 UIKitView,Android 平臺的視圖是 AndoirdView,本文全部描述都是針對 iOS 平臺,按官方的描述該功能仍是在發佈預覽階段,而且是很是昂貴的操做;如下是官方 API 文檔原文註釋:html
Embedding UIViews is still in release preview, to enable the preview for an iOS app add a boolean field with the key 'io.flutter.embedded_views_preview' and the value set to 'YES' to the application's Info.plist file. A list of open issued with embedding UIViews is available on Github. Embedding iOS views is an expensive operation and should be avoided when a Flutter equivalent is possible.
每一個技術點的出現必然有它的價值所在,因此即使 PlatfromView 目前存在一些問題,而且 Flutter 自己就是一個 UI 框架,一些業務場景下只能依賴於它完成,例如:地圖、原生廣告、WebView等等;因此 Flutter 開發者仍是得點亮 PlatformView 技能樹;java
在 Flutter1.12 版本中遇到過在 PageView、ListView 等容器視圖中將 PlatformView 移動到屏幕外,而且 Widget 沒銷燬的場景會引發引擎崩潰,因爲問題出在 Flutter 引擎內部,遇到問題的時候能夠作這三件事:ios
固然在業務迭代中一般優先選擇第三點曲線規避當前問題,而後給官方提 issue,定製引擎這個選項最好在有足夠把握的時候選擇,不嚴謹的改動可能會引發一系列問題;git
需求:建立一個能夠將黃色的 UIView 顯示到窗口的插件;
建立插件能夠經過命令行生成插件模板工程, 工程名只能用小寫:github
flutter create --template=plugin -i objc -a java platform_view
這裏建立的是 iOS 端使用 OC 語言 Android 端使用 Java 語言的插件,建立成功後能夠看到這樣的目錄結構:shell
在 lib 目錄下建立 color_view.dart 存放 UIKitView的一些操做,Flutter 能夠利用平臺通道 MethodChannel 與原平生臺進行數據交互,方法調用在發送以前被編碼爲二進制,接收到的二進制結果被解碼爲Dart值。api
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; const String singleColor = "singleColor"; class ColorView extends StatefulWidget { @override _ColorViewState createState() => _ColorViewState(); } class _ColorViewState extends State<ColorView> { /// 平臺通道,消息使用平臺通道在客戶端(UI)和宿主(平臺)之間傳遞 MethodChannel _channel; @override Widget build(BuildContext context) { return UiKitView( // 視圖類型,做爲惟一標識符 viewType: singleColor, // 建立參數:將會傳遞給 iOS 端側, 能夠傳遞任意類型參數 creationParams: "yellow", // 用於將creationParams編碼後再發送到平臺端。 // 這裏使用Flutter標準二進制編碼 creationParamsCodec: StandardMessageCodec(), // 原生視圖建立回調 onPlatformViewCreated: _onPlatformViewCreated, ); } /// 原生視圖建立回調操做 /// id 是原生視圖惟一標識符 void _onPlatformViewCreated(int id) { // 每一個 id 對應建立惟一的平臺通道 _channel = MethodChannel('singleColor_$id'); // 設置平臺通道的響應函數 _channel.setMethodCallHandler(_handleMethod); } /// 平臺通道的響應函數 Future<void> _handleMethod(MethodCall call) async { /// 視圖沒被裝載的狀況不響應操做 if (!mounted) { return Future.value(); } switch (call.method) { default: throw UnsupportedError("Unrecognized method"); } } }
使用 Xcode 編輯 iOS 平臺代碼以前,首先確保代碼至少被構建過一次,即從 IDE/編輯器執行示例程序,或在終端中執行如下命令:app
cd platform_view/example; flutter build ios --debug --no-codesign
打開 Platform_view/example/ios/Runner.xcworkspace
iOS 工程,插件的 iOS 平臺代碼位於項目導航中的這個位置:框架
Pods/Development Pods/platform_view/../../example/ios/.symlinks/plugins/platform_view/ios/Classes
此文件建立插件工程時生成的,在程序啓動的時候會將 AppDeleage 註冊進來, 這裏的 AppDeleage 繼承自 FlutterAppDelegate 遵照了 FlutterPluginRegistry, FlutterAppLifeCycleProvider 協議,前者爲了提供應用程序上下文和註冊回調的方法,後者爲了方便後續在插件中獲取應用生命週期事件;async
#import "PlatformViewPlugin.h" #import "PlatfromViewFactory.h" @implementation PlatformViewPlugin /// 註冊插件 /// @param registrar 提供應用程序上下文和註冊回調的方法 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { // 註冊視圖工廠 // 綁定工廠惟一標識符這裏與 Flutter UIKitView 所使用 viewType 一致 [registrar registerViewFactory:[[PlatfromViewFactory alloc] initWithMessenger:[registrar messenger]] withId:@"singleColor"]; } @end
#import <Foundation/Foundation.h> #import <Flutter/Flutter.h> NS_ASSUME_NONNULL_BEGIN @interface PlatfromViewFactory : NSObject<FlutterPlatformViewFactory> /// 初始化視圖工廠 /// @param messager 用於與 Flutter 傳輸二進制消息通訊 - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager; @end NS_ASSUME_NONNULL_END #import "PlatfromViewFactory.h" #import "PlatformView.h" @interface PlatfromViewFactory () /// 用於與 Flutter 傳輸二進制消息通訊 @property (nonatomic, strong) NSObject<FlutterBinaryMessenger> *messenger; @end @implementation PlatfromViewFactory - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager { self = [super init]; if (self) { self.messenger = messager; } return self; } #pragma mark - FlutterPlatformViewFactory /// 建立一個「FlutterPlatformView」 /// 由iOS代碼實現,該代碼公開了一個用於嵌入Flutter應用程序的「UIView」。 /// 這個方法的實現應該建立一個新的「UIView」並返回它。 /// @param frame Flutter經過其佈局widget來計算得來 /// @param viewId 視圖的惟一標識符,建立一個 UIKitView 該值會+1 /// @param args 對應Flutter 端UIKitView的creationParams參數 - (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { PlatformView *platformView = [[PlatformView alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:self.messenger]; return platformView; } /// 使用Flutter標準二進制編碼 - (NSObject<FlutterMessageCodec> *)createArgsCodec { return [FlutterStandardMessageCodec sharedInstance]; } @end
Flutter 端 UIKitView 的 viewType 與 工廠 ID 相同才能創建關聯,工廠的核心方法 createWithFrame,這裏三個參數都是由 Flutter 端傳遞過來的,UIKitView 的大小是由父 Widget 決定的,frame也就是 Flutter 經過其佈局 widget 來計算得來, viewId 是建立一個 UIKitView 該值會+1,而且是惟一的,args 對應 Flutter端 UIKitView 的 creationParams 參數;
PlatformView 繼承自 FlutterPlatformView 協議,工廠調用 PlatformView 對象來建立真正的 view 實例:
#import <Foundation/Foundation.h> #import <Flutter/Flutter.h> NS_ASSUME_NONNULL_BEGIN @interface PlatformView : NSObject<FlutterPlatformView> - (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger; @end NS_ASSUME_NONNULL_END #import "PlatformView.h" @interface PlatformView () /// 視圖 @property (nonatomic, strong) UIView *yellowView; /// 平臺通道 @property (nonatomic, strong) FlutterMethodChannel *channel; @end @implementation PlatformView - (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger { if ([super init]) { /// 初始化視圖 self.yellowView = [[UIView alloc] init]; self.yellowView.backgroundColor = UIColor.yellowColor; /// 這裏的channelName是和Flutter 建立MethodChannel時的名字保持一致的,保證一個原生視圖有一個平臺通道傳遞消息 NSString *channelName = [NSString stringWithFormat:@"singleColor_%lld", viewId]; self.channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; // 處理 Flutter 發送的消息事件 [self.channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { if ([call.method isEqualToString:@""]) { } }]; } return self; } #pragma mark - FlutterPlatformView /// 返回真正的視圖 - (UIView *)view { return self.yellowView; } @end
在 example工程中的 lib/main.dart 中使用封裝好的 ColorView:
import 'package:flutter/material.dart'; import 'package:platform_view/color_view.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('PlatformView Plugin'), ), body: Center( // 因爲原生視圖的大小由父 Widget 決定, // 這裏添加 Container 做爲父 Widget 並設置寬高爲 100 child: Container( width: 100.0, height: 100.0, child: ColorView(), ), ), ), ); } }
因爲嵌入 UIViews 仍在版本預覽中,默認此功能是關閉的,須要在 info.pilst
進行配置,開啓嵌入原生視圖:
<key>io.flutter.embedded_views_preview</key> <true/>
寬高各 100 的黃色 UIView 就顯示出來了,這裏只是舉了個最簡單的場景,能夠根據業務需求定製和原平生臺的交互。
剛剛咱們運行應用前在 info.plist 配置了開啓原生視圖預覽,能夠看到源碼中獲取了開啓狀態,在沒開啓的時候返回 nullptr ,嵌入式視圖要求 GPU 和平臺視圖的線程相同,即主線程;不開啓則是由 GPU 線程繪製畫布上的 UI;
// The name of the Info.plist flag to enable the embedded iOS views preview. const char* const kEmbeddedViewsPreview = "io.flutter.embedded_views_preview"; bool IsIosEmbeddedViewsPreviewEnabled() { return [[[NSBundle mainBundle] objectForInfoDictionaryKey:@(kEmbeddedViewsPreview)] boolValue]; } ExternalViewEmbedder* IOSSurfaceSoftware::GetExternalViewEmbedder() { if (IsIosEmbeddedViewsPreviewEnabled()) { return this; } else { return nullptr; } } if (flutter::IsIosEmbeddedViewsPreviewEnabled()) { // Embedded views requires the gpu and the platform views to be the same. // The plan is to eventually dynamically merge the threads when there's a // platform view in the layer tree. // For now we use a fixed thread configuration with the same thread used as the // gpu and platform task runner. // TODO(amirh/chinmaygarde): remove this, and dynamically change the thread configuration. // https://github.com/flutter/flutter/issues/23975 flutter::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform fml::MessageLoop::GetCurrent().GetTaskRunner(), // gpu _threadHost.ui_thread->GetTaskRunner(), // ui _threadHost.io_thread->GetTaskRunner() // io ); // Create the shell. This is a blocking operation. _shell = flutter::Shell::Create(std::move(task_runners), // task runners std::move(settings), // settings on_create_platform_view, // platform view creation on_create_rasterizer // rasterzier creation ); } else { flutter::TaskRunners task_runners(threadLabel.UTF8String, // label fml::MessageLoop::GetCurrent().GetTaskRunner(), // platform _threadHost.gpu_thread->GetTaskRunner(), // gpu _threadHost.ui_thread->GetTaskRunner(), // ui _threadHost.io_thread->GetTaskRunner() // io ); // Create the shell. This is a blocking operation. _shell = flutter::Shell::Create(std::move(task_runners), // task runners std::move(settings), // settings on_create_platform_view, // platform view creation on_create_rasterizer // rasterzier creation ); }
接着來看看 UIKitView 建立後是怎麼到 iOS 端側的:
getNextPlatformViewId實際上的操做是內部記錄了 viewId 的值,每次調用後+1;int getNextPlatformViewId() => _nextPlatformViewId++;
後面的 UiKitViewController 看起來就是核心控制層了;
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) { ... NSDictionary<NSString*, id>* args = [call arguments]; // 獲取 viewid long viewId = [args[@"id"] longValue]; // 獲取 viewType std::string viewType([args[@"viewType"] UTF8String]); ... // 經過 viewType 獲取視圖工廠 NSObject<FlutterPlatformViewFactory>* factory = factories_[viewType].get(); ... id params = nil; // 解碼參數 if ([factory respondsToSelector:@selector(createArgsCodec)]) { NSObject<FlutterMessageCodec>* codec = [factory createArgsCodec]; if (codec != nil && args[@"params"] != nil) { FlutterStandardTypedData* paramsData = args[@"params"]; params = [codec decode:paramsData.data]; } } // 經過視圖工廠建立嵌入視圖 NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:params]; views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]); // 將嵌入視圖添加到FlutterTouchInterceptingView中, // FlutterTouchInterceptingView主要負責處理手勢轉發和拒絕部分手勢, FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc] initWithEmbeddedView:embedded_view.view flutterViewController:flutter_view_controller_.get()] autorelease]; // 存儲視圖 touch_interceptors_[viewId] = fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]); root_views_[viewId] = fml::scoped_nsobject<UIView>([touch_interceptor retain]); result(nil); }
在建立視圖流程中引擎還默認添加了 FlutterOverlayView,目的是防止原生視圖遮擋 Flutter 視圖,原生視圖層級之上 Flutter 視圖都會繪製在 FlutterOverlayView 上,同一層級的視圖仍是繪製在 FlutterView 上面,這裏 FlutterView 和 FlutterOverlayView 都是 CAEAGLLayer,用於渲染 Flutter 視圖。