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 视图。