Flutter使用 PlatforView 显示 iOS 原生 View

前言

  • 2018年一直在使用 flutter 写项目,从0.2.0开始到现在1.0版本的发布,终于开始慢慢的爬出坑位了,但是因为部分控件感觉还是不如原生控件好用,一直在摸索怎么将原生view 可以放在 flutter 中并且不会遮挡住 flutter 的 widget。终于,看到官网提供了 PlatformView部件,因为我本身是一名 iOS 开发人员,这里只提供 iOS 的教程,Android 开发教程在这里

什么是 PlatformView?

  • PlatformView是 flutter 官方提供的一个可以嵌入 Android 和 iOS 平台原生 view 的小部件。
  • 在我们实际开发中,我们遇到一些 flutter 官方没有提供的插件可以自己创建编写插件来实现部分功能,但是原生View在 flutter 中会遮挡住flutter 中的小部件,比如你想使用高德地图sdk、视频播放器、直播等原生控件,就无法很好的与 flutter 项目结合。
  • 之前知道flutter 给 Android(google 的亲儿子)提供了 AndroidView可以实现将 view 存放到部件中,教程也不少,无奈,iOS (毕竟不是亲的)在网上使用 UiKitView的教程太少,目前就只看到日本的一个作者用 swift写的 教程,终于有了可以参考的 Demo,下面我就用 Object-C 来说一下教程:

制作插件

我们需要创建一个 项目插件,我这里使用 默认的 Object-C和 Java语言。
Plugin
activity_indicator.dart

首先,我将创建一个StatefulWidget类,在class下显示本机视图。
使用文件名activity_indicator.dart编写以下代码。


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';



typedef void UIActivityIndicatorWidgetCreatedCallback(ActivityIndicatorController controller);

class ActivityIndicatorController {
  ActivityIndicatorController._(int id)
      : _channel = MethodChannel('plugins/activity_indicator_$id');

  final MethodChannel _channel;

  Future<void> start() async {
    return _channel.invokeMethod('start');
  }

  Future<void> stop() async {
    return _channel.invokeMethod('stop');
  }
}

class UIActivityIndicator extends StatefulWidget{

  const UIActivityIndicator({
    Key key,
    this.onActivityIndicatorWidgetCreated,
    this.hexColor,

  }):super(key:key);

  final UIActivityIndicatorWidgetCreatedCallback onActivityIndicatorWidgetCreated;
  final String hexColor;

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _UIActivityIndicatorState();
  }

}

class _UIActivityIndicatorState extends State<UIActivityIndicator>{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    if(defaultTargetPlatform == TargetPlatform.iOS){
      return UiKitView(
        viewType: "plugins/activity_indicator",
        onPlatformViewCreated:_onPlatformViewCreated,
        creationParams: <String,dynamic>{
          "hexColor":widget.hexColor,
          "hidesWhenStopped":true,

        },
        creationParamsCodec: new StandardMessageCodec(),

      );

    }
    return Text('activity_indicator插件尚不支持$defaultTargetPlatform ');
  }

  void _onPlatformViewCreated(int id){
    if(widget.onActivityIndicatorWidgetCreated == null){
      return;
    }
    widget.onActivityIndicatorWidgetCreated(new ActivityIndicatorController._(id));
  }

}
调用 iOS视图

UIKitView用于调用iOS视图,如下所示。
对于指定的参数,viewType用于确定本机端的目标View的返回。
对于Android,我们使用AndroidView但指定viewType不会更改。

此外,onPlatformViewCreated可以将ActivityIndi​​catorController与UIActivityIndi​​cator小部件一起使用。
要将参数传递给本机端,请使用creationParams。

  Widget build(BuildContext context) {
    // TODO: implement build
    if(defaultTargetPlatform == TargetPlatform.iOS){
      return UiKitView(
        viewType: "plugins/activity_indicator",
        onPlatformViewCreated:_onPlatformViewCreated,
        creationParams: <String,dynamic>{
          "hexColor":widget.hexColor,
          "hidesWhenStopped":true,

        },
        creationParamsCodec: new StandardMessageCodec(),

      );

    }
    return Text('activity_indicator插件尚不支持$defaultTargetPlatform ');
  }
从 Flutter 运行原生代码

使用MethodChannel从Flutter执行本机代码。
这也会编写接收MethodChannel和invokeMethod参数的代码,并在本机端执行相应的处理。
这次实现它,以便可以通过ActivityIndi​​catorController执行本机代码。

class ActivityIndicatorController {
  ActivityIndicatorController._(int id)
      : _channel = MethodChannel('plugins/activity_indicator_$id');

  final MethodChannel _channel;

  Future<void> start() async {
    return _channel.invokeMethod('start');
  }

  Future<void> stop() async {
    return _channel.invokeMethod('stop');
  }
}
main.dart

接下来,编辑example / main.dart并创建一个屏幕。
我将使用我之前创建的UIActivityIndi​​cator小部件。

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:activity_indicator/activity_indicator.dart';

void main() => runApp(MaterialApp(
  home: ActivityIndicatorExample(),
));

class ActivityIndicatorExample extends StatelessWidget{
  
  ActivityIndicatorController controller;
  
  void _onActivityIndicatorControllerCreated(ActivityIndicatorController _controller){
    controller = _controller;
  }
  
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: const Text("加载测试"),),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: <Widget>[
          new Container(
            child: new Stack(
              children: <Widget>[
                UIActivityIndicator(
                  hexColor: "FF0000",
                  onActivityIndicatorWidgetCreated: _onActivityIndicatorControllerCreated,
                ),
                new Container(
                  alignment: Alignment.center,
                  child: new Text("我是flutter控件,没有被遮挡~"),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.only(left: 45.0,right: 45.0,top: 0.0,bottom: 50.0),
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                FloatingActionButton(
                  onPressed: (){
                    controller.start();
                  },
                  child: new Text("Start"),
                ),
                FloatingActionButton(
                  onPressed: (){
                    controller.stop();
                  },
                  child: new Text("Stop"),
                )
              ],
            ),
          )
        ],
      ),
    );
  }

}

iOS 端实现


FlutterActivityIndicator.h

新建类,提供FlutterPlatformView和FlutterPlatformViewFactory协议

#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>

NS_ASSUME_NONNULL_BEGIN

@interface FlutterActivityIndicatorController : NSObject<FlutterPlatformView>

- (instancetype)initWithWithFrame:(CGRect)frame
                   viewIdentifier:(int64_t)viewId
                        arguments:(id _Nullable)args
                  binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;

@end

@interface FlutterActivityIndicatorFactory : NSObject<FlutterPlatformViewFactory>

- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messager;

@end
NS_ASSUME_NONNULL_END
从 Flutter 运行原生代码

要从Flutter端执行本机代码,可以按如下方式使用MethodChannel。
它产生以前MethodChannel,当您从ActivityIndi​​catorController时,InvokeMethod,onMethodCall被调用,所以你遇到在参数中指定的字符串,以及运行过程中看到它的价值。

 _viewId = viewId;
 NSString* channelName = [NSString stringWithFormat:@"plugins/activity_indicator_%lld", viewId];
_channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall *  call, FlutterResult  result) {
     [weakSelf onMethodCall:call result:result];
}];


-(void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
    if ([[call method] isEqualToString:@"start"]) {
        [_indicator startAnimating];
    }else
    if ([[call method] isEqualToString:@"stop"]){
        [_indicator stopAnimating];
    }
    else {
        result(FlutterMethodNotImplemented);
    }
}

将参数从 Flutter 传递到 iOS

由于Flutter端的creationParams指定的值是args,因此将其转换为类型并设置为UIActivityIndi​​catorView的属性。

NSDictionary *dic = args;
NSString *hexColor = dic[@"hexColor"];
 bool hidesWhenStopped = [dic[@"hidesWhenStopped"] boolValue];
        
_indicator = [[UIActivityIndicatorView alloc]init];
_indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
_indicator.color = [UIColor colorWithHexString:hexColor];
_indicator.hidesWhenStopped = hidesWhenStopped;
ActivityIndicatorPlugin.m

自动生成文件中,只需要这样写

#import "ActivityIndicatorPlugin.h"
#import "FlutterActivityIndicator.h"

@implementation ActivityIndicatorPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    [registrar registerViewFactory:[[FlutterActivityIndicatorFactory alloc] initWithMessenger:registrar.messenger] withId:@"plugins/activity_indicator"];
    
}
@end

要保证你的viewId指定的字符串与你 flutter 端代码的
ViewType指定的字符串相匹配

最重要的一步操作

要在你的 info.plist中添加

<key>io.flutter.embedded_views_preview</key>
<true/>

要求必须这样设置
https://github.com/flutter/flutter/issues/19030#issuecomment-437534853

演示


演示 Demo.gif

Demo 地址

下载地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容