以前介绍过在原生工程内嵌入Flutter,以页面形式或者View的形式嵌入都是能够的,最近看Flutter源码发现Flutter还支持在Flutter布局中嵌入原生View,这个特性在文档中尚未介绍,可是确实是一个很是实用的特性,好比困扰已久的地图实现,有了这个特性咱们就能够在Flutter布局中嵌入双平台的原生高德地图或百度地图,甚至是相机预览视频通话SDK等。
本篇一个简单的TextView为示例,介绍如何在Flutter工程中嵌入原生组件。
android
原生组件扩展比较规范的写法是建立插件工程,而后让Flutter工程引入插件工程使用,本篇为了方便,直接在Flutter工程编写组件并注册,插件工程的开发之后再介绍。
使用AndroidStudio建立一个普通的Flutter工程,修改main.dar文件,移除没必要要的代码便于演示,整理后代码以下:ios
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
),
);
}
}
复制代码
添加原生组件的流程基本是这样的:
1.实现原生组件PlatformView提供原生view 2.建立PlatformViewFactory用于生成PlatformView 3.建立FlutterPlugin用于注册原生组件web
在FLutter工程生成了几个文件夹,lib是放Flutter工程代码,android和ios文件夹分别是对应的双平台的原生工程,这里直接打开Android工程目录,项目默认生成了GeneratedPluginRegistrant和MainActivity两个文件,GeneratedPluginRegistrant不要动,在和MainActivity的包下新建自定义View,Flutter的原生View不能直接继承自View,须要实现提供的PlatformView接口:app
public class MyView implements PlatformView {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
TextView myNativeView = new TextView(context);
myNativeView.setText("我是来自Android的原生TextView");
this.myNativeView = myNativeView;
}
@Override
public View getView() {
return myNativeView;
}
@Override
public void dispose() {
}
}
复制代码
这是一个包装类,在实现的getView方法中返回原生的View对象给Flutter,这里便于演示,返回一个TextView。less
接下来建立PlatformViewFactory,建立一个类继承自PlatformViewFactory:async
public class MyViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
public MyViewFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
@SuppressWarnings("unchecked")
@Override
public PlatformView create(Context context, int id, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
return new MyView(context, messenger, id, params);
}
复制代码
在create方法中可以获取到三个参数,args是由Flutter传过来的自定义参数,这里暂时用不到。ide
建立一个插件类MyViewFlutterPlugin,并在类的静态方法中写上注册逻辑供调用:布局
public class MyViewFlutterPlugin {
public static void registerWith(PluginRegistry registry) {
final String key = MyViewFlutterPlugin.class.getCanonicalName();
if (registry.hasPlugin(key)) return;
PluginRegistry.Registrar registrar = registry.registrarFor(key);
registrar.platformViewRegistry().registerViewFactory("plugins.nightfarmer.top/myview", new MyViewFactory(registrar.messenger()));
}
}
复制代码
上面代码中使用了plugins.nightfarmer.top/myview
这样一个字符串,这是组件的注册名称,在Flutter调用时须要用到,你可使用任意格式的字符串。 在MainActivity的onCreate方法中增长注册调用性能
MyViewFlutterPlugin.registerWith(this);
复制代码
由于这里是直接在Flutter工程中编写的,因此也能够直接把注册逻辑写在Activity中,为了和插件工程的注册流程保持一致,仍是建议抽出来写。ui
原生View的调用很是简单,在使用Android平台的view只须要建立AndroidView
组件并告诉它组件的注册注册名称便可:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(viewType: 'plugins.nightfarmer.top/myview'),
),
);
}
}
复制代码
由于只是实现了Android平台,因此这里直接调用了AndroidView,若是你是双平台的实现,则能够经过引入package:flutter/foundation.dart
包,并判断defaultTargetPlatform
是TargetPlatform.android
仍是TargetPlatform.iOS
来引入不一样平台的实现。
某些状况下,须要给原生组件提供一些初始化参数,好比webview的url,好比地图的中心坐标,又好比上面示例的中文本内容,咱们传入一个map便可实现:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "经过参数传入的文本内容",
},
creationParamsCodec: const StandardMessageCodec(),
),
),
);
}
}
复制代码
creationParams
传入了一个map参数,并由原生组件接收,creationParamsCodec
传入的是一个编码对象这是固定写法。 而后在原生组件中接收参数并初始化TextView的文本:
public class MyView implements PlatformView {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
TextView myNativeView = new TextView(context);
myNativeView.setText("我是来自Android的原生TextView");
this.myNativeView = myNativeView;
if (params.containsKey("myContent")) {
String myContent = (String) params.get("myContent");
myNativeView.setText(myContent);
}
}
...
}
复制代码
有一点须要注意的是,原生组件初始化的参数并不会随着setState重复赋值,也就是说这种是init参数。
关于如何更改已经实例化的原生组件的状态,能够经过MethodCall来实现,看下面
首先让原始组件实现MethodCallHandler
接口:
public class MyView implements PlatformView, MethodChannel.MethodCallHandler {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
...
MethodChannel methodChannel = new MethodChannel(messenger, "plugins.nightfarmer.top/myview_" + id);
methodChannel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// 在接口的回调方法中能够接收到来自Flutter的调用
}
...
}
复制代码
而后在dart代码中作以下处理:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "经过参数传入的文本内容",
},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: onMyViewCreated,
),
),
);
}
MethodChannel _channel;
void onMyViewCreated(int id) {
_channel = new MethodChannel('plugins.nightfarmer.top/myview_$id');
setMyViewText();
}
Future<void> setMyViewText(String text) async {
assert(text != null);
return _channel.invokeMethod('setText', text);
}
}
复制代码
经过onPlatformViewCreated
回调,监听原始组件成功建立,并可以在回调方法的参数中拿到当前组件的id,这个id是系统随机分配的,而后经过这个分配的id加上咱们的组件名称最为前缀建立一个和组件通信的MethodChannel,拿到channel对象以后就能够经过invokeMethod方法向原生组件发送消息了,这里这里发送的是‘setText’这个消息,并带上文本内容,下面在原生组件中处理消息的接收逻辑。
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if ("setText".equals(methodCall.method)) {
String text = (String) methodCall.arguments;
myNativeView.setText(text);
result.success(null);
}
}
复制代码
onMethodCall的处理方式和正常的插件扩展是一致的,这里再也不赘述。
经过一个ListView来实例化多个原生组件,看看效果如何:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder(
itemBuilder: (context, index) {
return Container(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "经过参数传入的文本内容$index",
},
creationParamsCodec: const StandardMessageCodec(),
),
height: 100,
);
},
itemCount: 100,
),
);
}
}
复制代码
这样写虽然跑起来了,ListView也确实可以正常滑动,可是可以感觉到明显的掉帧,可见在一个界面中实例化多个原生组件的状况对性能的影响很是的大,也不建议在实际开发中大量引入原生组件,由于除去地图/WebView等特殊状况,基本上原生能实现的UI效果Flutter的UI引擎都能实现。
在开发原生组件时,Flutter的热加载是无效的,由于每次都须要编译原生工程才能使之生效。另外我这里的Mac环境用Genymotion是没法正常运行的,须要使用真机并不使用
--enable-software-rendering
参数才能够。
本篇完。
更多干货移步个人我的博客 www.nightfarmer.top/