Flutter應用中顯示iOS/Android原生視圖

在我們開發Flutter應用時,時常會遇到以下問題:

  1. Flutter內置(或者第三方)提供的Widget不足以實現複雜交互
  2. 已經以原生方式實現了複雜的界面交互,只是想在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端代碼就是這些,下面切換到原生代碼端。

原生端

原生端只需要做以下兩件事:

  1. 創建視圖工廠對象,並在工廠方法中返回需要的原生視圖對象。
  2. 在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原生視圖的嵌入,稍後會補充。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章