在我們開發Flutter應用時,時常會遇到以下問題:
- Flutter內置(或者第三方)提供的Widget不足以實現複雜交互
- 已經以原生方式實現了複雜的界面交互,只是想在Flutter應用中嵌套原生寫好的視圖
此時就需要在Flutter的Widget樹種直接嵌入原生視圖。本文以iOS爲例,來講解如何實現。
Flutter端
創建一個新的Flutter應用項目,在lib/main.dart中修改_MyHomePageState類的build方法,在界面中嵌入UiKitView,代碼如下:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
color: Colors.blue,
),
),
Expanded(
flex: 2,
child: getMyPatformView(),
),
],
),
);
}
Widget getMyPatformView() {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'MyUiKitView',
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'MyUiKitView',
);
}
return Text('$defaultTargetPlatform is not yet supported by this plugin');
}
}
注意到代碼中的UiKitView,它用來嵌入iOS原生視圖。根據Flutter官方文檔,UiKitView控件會自動填所有可用區域,因此其父控件必須具有明確的區域尺寸。這裏其寬度和屏幕寬度相同,高度爲屏幕的2/3。其帶有一個viewType屬性,其值用於唯一對應某一類型的原生視圖。Flutter端代碼就是這些,下面切換到原生代碼端。
原生端
原生端只需要做以下兩件事:
- 創建視圖工廠對象,並在工廠方法中返回需要的原生視圖對象。
- 在AppDelegate對象的App啓動方法中,使用viewType屬性值註冊視圖工廠對象。
創建視圖工廠並返回原生視圖
打開Flutter項目根目錄下的ios文件夾,雙擊Runner.xcworkspace打開原生項目。創建一個新的視圖工廠類MyPlatformViewFactory,其需要繼承自NSObject類,並遵從FlutterPlatformViewFactory協議,代碼如下:
MyPlatformViewFactory.h:
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyPlatformViewFactory : NSObject <FlutterPlatformViewFactory>
@end
NS_ASSUME_NONNULL_END
MyPlatformViewFactory.m:
#import "MyPlatformViewFactory.h"
#import "MyPlatformViewObject.h"
@implementation MyPlatformViewFactory
- (NSObject <FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args
{
MyPlatformViewObject *myPlatformViewObject = [[MyPlatformViewObject alloc] initWithFrame:frame viewId:viewId args:args];
return myPlatformViewObject;
}
@end
FlutterPlatformViewFactory協議中的工廠方法createWithFrame:viewIdentifier:arguments:是一個必須實現的方法,該方法生成一個遵從FlutterPlatformView協議的對象並返回。FlutterPlatformView協議用於將原生視圖嵌入到Flutter的widget樹中,代碼如下:
MyPlatformViewObject.h:
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
@interface MyPlatformViewObject : NSObject <FlutterPlatformView>
- (id)initWithFrame:(CGRect)frame viewId:(int64_t)viewId args:(id)args;
@end
MyPlatformViewObject.m:
#import <CoreGraphics/CoreGraphics.h>
#import "MyPlatformViewObject.h"
@implementation MyPlatformViewObject
{
CGRect _frame;
int64_t _viewId;
id _args;
UILabel *_subLabel;
}
- (id)initWithFrame:(CGRect)frame viewId:(int64_t)viewId args:(id)args
{
if (self = [super init])
{
_frame = frame;
_viewId = viewId;
_args = args;
}
return self;
}
- (UIView *)view
{
UIView *myNativeView = [[UIView alloc] initWithFrame:_frame];
myNativeView.backgroundColor = [UIColor purpleColor];
_subLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 480, 44)];
_subLabel.text = @"我是原生Label!!";
_subLabel.textColor = [UIColor whiteColor];
_subLabel.numberOfLines = 0;
[myNativeView addSubview:_subLabel];
UIButton *subBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[subBtn setFrame:CGRectMake(20, 120, 200, 200)];
[subBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[subBtn setTitle:@"點我試試!" forState:UIControlStateNormal];
subBtn.titleLabel.font = [UIFont boldSystemFontOfSize:25.0];
[subBtn addTarget:self action:@selector(subBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
[myNativeView addSubview:subBtn];
return myNativeView;
}
- (void)subBtnClicked:(id)sender
{
_subLabel.text = [NSString stringWithFormat:@"%@如果你覺得這篇文章有用,請給我點個贊吧!!", _subLabel.text];
}
@end
FlutterPlatformView協議中有一個必須實現的方法view,該方法用於真正生成原生視圖樹並返回根視圖,這裏創建了一個紫色背景的myNativeView,並添加了UILabel、UIButton及對應點擊處理。
註冊視圖工廠對象
在AppDelegate的application:didFinishLaunchingWithOptions:方法內,創建MyPlatformViewFactory視圖工廠對象並註冊,代碼如下:
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#import "MyPlatformViewFactory.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[GeneratedPluginRegistrant registerWithRegistry:self];
MyPlatformViewFactory *myPlatformViewFactory = [[MyPlatformViewFactory alloc] init];
[[self registrarForPlugin:@"Lipuzhi"] registerViewFactory:myPlatformViewFactory withId:@"MyUiKitView"];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
App啓動後,調用registrarForPlugin:方法註冊插件,要求插件名字唯一。然後調用registerViewFactory:withId:方法註冊視圖工廠,傳入對應的工廠對象和Flutter端對應的viewType值即可。
修改項目配置
此時如果直接運行Flutter端,發現App無法顯示原生視圖內容,同時控制檯報錯:
Trying to embed a platform view but the PrerollContext does not support embedding.
因爲在Flutter中嵌入原生視圖的開銷很大,默認情況下是不開放此功能的,因此需要手動修改info.plist文件。在Xcode中右鍵單擊info.plist -> Open As -> Source Code,這樣會以源代碼方式打開:
在<dict>和</dict>區塊內添加以下代碼:
<key>io.flutter.embedded_views_preview</key>
<true/>
保存後即可。
運行結果
回到Flutter端並運行,實際效果如圖。
Demo源碼地址:點擊下載Demo
Android原生視圖的嵌入,稍後會補充。