2020-05-13 17:33:34
Flutter,百度地图,原生插件
简介
先介绍一下,这个插件只是教学目的,并不完整。
目前提供了
- 显示地图
- 获取地图中心坐标
- 定位
- 距离计算
写这个插件的时候,
我只学了 Flutter 一个月,Android 没学过,也没掌握 Java 。
没想到文章越写越多,分成了3部分,这是最后一部分,
前2部分链接:
地图插件的关键是 Flutter 端的 AndroidView 和 Android 端的 PlatformView 。
Flutter 如何显示 Android 视图
要想让 Flutter 显示一个安卓视图 。
- 安卓端需要提供一个 PlatformView 来生成安卓视图,即 View 。
- PlatformView 需要用 PlatformViewFactory 来创建。
- 最后通过 PlatformViewRegistry 登记 PlatformViewFactory 。
- 在Flutter 端用一个 AndroidView 去请求 Android 端的 PlatformView 。
Flutter 端
AndroidView 常用的参数:
- viewType
一个字符串,指定 Android 端登记的某个视图。就像 MethodChannel 的 name 参数一样。 - onPlatformViewCreated
一个回调函数,在安卓端的视图准备好后调用。接受一个 int 型参数。 - creationParams
传递给安卓端的参数。类型为 dynamic 。可选。 - creationParamsCodec
给 creationParams 编码的编码器,一般是 StandardMessageCodec 。如果指定了 creationParams ,这个不能为空 。
Flutter 请求安卓视图的时候,会传递以下参数:
- id 这个 id 从0开始,每次 AndroidView 重建后会加一。重建前会检查 viewType 和之前的是否相同。
- viewType
- width
- height
- direction
- params 如果 AndroidView 的 creationParams 不会空,会加上这个。
向安卓发送请求,就是通过我们熟悉的 MethodChannel 。
安卓端会返回一个 textureId 供 TextureLayer 使用。
Android 端
Android 端收到请求后,
会在已登记的 PlatformViewFactory 中找到和传过来的 viewType 对应的那个。
有了 PlatformViewFactory ,就可以得到我们的地图了。还会创建一个 SurfaceTexture ,并给它分配一个 ID 。这个 ID 也是从 0 开始,每次加一。
这个 ID 将作为 Flutter 端请求的结果返回。
看网上的文章,有看到过 GPU 中的纹理 ID 。会是这个从0开始的 ID 吗?
有了这个 ID ,Flutter 就可以直接显示 Android 渲染的视图,不需要经过 CPU 传数据了吗?
不去研究了。
感觉 SurfaceTexture 和 View 应该有着什么关系。
不过目前没发现,也没觉得需要知道。
下面从登记开始,说一下需要知道的。
PlatformViewRegistry
要想登记 PlatformViewFactory ,
需要用到 PlatformViewRegistry 的 registerViewFactory()
方法。
PlatformViewRegistry 是在应用启动后,创建 FlutterEngine 时创建的。
怎样得到它?
如果我们可以直接得到 FlutterEngine 。
例如在 FlutterActivity 的configureFlutterEngine()
方法中。
那么可以先用 FlutterEngine 的getPlatformViewsController()
方法
得到 PlatformViewsController 。
然后用 PlatformViewsController 的getRegistry()
得到 PlatformViewRegistry 。如果我们在写插件,可以在
onAttachedToEngine()
中
通过 FlutterPluginBinding 的getPlatformViewRegistry()
获得。
PlatformViewRegistry 的 registerViewFactory()
方法接受2个参数,
- 第一个是 viewType ,字符串类型。是一个唯一的标识符,用来标识一个 PlatformViewFactory 。
- 第二个是 PlatformViewFactory 。用来创建 PlatformView 。
它有个 boolean 类型的返回值。
登记的时候会通过 viewType 检查是否已经登记过,
如果登记过返回 false,否则返回 true 。
PlatformViewFactory
创建 PlatformViewFactory 需要指定一个解码器,用于解码 Flutter 端传递过来的参数。
这个解码器要与 Flutter 端的编码器对应,一般是 StandardMessageCodec 。
我们需要重写 create()
方法。
有3个可用的参数:
- context
最开始是个 FlutterActivity ,经过层层包装,最后是一个 ContextWrapper 。
包含了 InputMethodManager ,InvocationHandler,WindowManager, 重写了getSystemService()
,
这些我都不懂。 - viewId
Flutter 端传递过来的 id 。 - args
解码过的 Flutter 端传递过来的参数
需要返回一个 PlatformView 。
PlatformView
需要重写2个方法:
-
getView()
返回一个 View ,就是我们的百度地图啦。 -
dispose()
做些清理工作。
当 Flutter 端的 AndroidView 被 dispose 或重建前,会调用这里的 dispose()
。
下面献上一个实例。
新建一个 Flutter 插件项目
我这里项目名字是 flutter_plugin_demo
。
特定平台的包名是 xyz.waixingjiandie.flutter_plugin_demo
。
新项目的目录结构
插件的 pubspec.yaml
:
# /pubspec.yaml
name: flutter_plugin_demo
flutter:
plugin:
platforms:
android:
package: xyz.waixingjiandie.flutter_plugin_demo
pluginClass: FlutterPluginDemoPlugin # 指定了 Android 插件的类名字
ios:
pluginClass: FlutterPluginDemoPlugin
/lib/flutter_plugin_demo.dart
这个是插件的API,对应 “app-facing package”。
/android/
这个目录是Android平台的代码,对应 “platform package”。
/example/
这里是一个依赖我们插件的 Flutter 应用示例,向使用者展示如何使用我们的插件。
这个示例是可以直接运行的,官网建议编写插件前,先运行一遍。
打开Android模块
在 Writing custom platform-specific code 这个不是插件的教程中。
打开的路径是 /android/
。
而 Developing packages & plugins 这个插件教程中,
打开的是 /example/android/
。
没时间学太多 Android 知识,就不管为什么了。
如果打开的是 /android/
,会发现安卓代码中导入的 Flutter 包无效。
打开 /example/android/
的话,也可以访问 /android/
,并且 Flutter 包也没问题。
在我们的项目上右键,选择 Flutter -> Open Android module in Android Studio 。
打开的项目和选择 /example/android/
是一样的。
那我就以这种方式打开安卓模块吧。
打开后,我们要面向的路径是 /android/
。
添加百度地图SDK
根据百度地图文档 将地图 SDK 添加到项目中。
删掉多余的东西
打开 FlutterPluginDemoPlugin 类所在的文件。
示例插件实现了 FlutterPlugin
和 MethodCallHandler
。
之所以实现 MethodCallHandler
是因为示例程序用到了原生方法。
为了兼容,类中还有个多余的 registerWith()
方法。
把多余的去掉后:
package xyz.waixingjiandie.flutter_plugin_demo;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
public class FlutterPluginDemoPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
}
参考 百度地图文档 ,要显示地图是需要 Activity 的。
那么我们的插件就要实现 ActivityAware 。
实现 ActivityAware
public class FlutterPluginDemoPlugin implements FlutterPlugin, ActivityAware {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
}
}
我们的插件又多了4个要重写的方法。
初始化地图 SDK
要在哪里初始化 SDK 呢?
重写的6个方法中,
onAttachedToEngine()
是在 FlutterPlugin 关联到 FlutterEngine 时调用的。
发生在登记插件时 PluginRegistry 的 add()
方法中。
FlutterPlugin 关联到 FlutterEngine 后,
会检查这个 FlutterPlugin 是否实现了 ActivityAware 。
如果是,再看 FlutterEngine 是否关联了一个 Activity 。
如果是,那么执行 ActivityAware 中的 onAttachedToActivity()
。
有2个检查,由于我实现了 ActivityAware ,所以第一个检查通过。
对于第2个,要从应用的启动说起。
启动一个安卓应用,启动的通常是一个 Activity 。
我们 /example/
中的示例是可以启动的。
从示例中可以看到这个 Activity 是 FlutterActivity 。
如果没有重写 FlutterActivity 的 shouldAttachEngineToActivity()
方法的话,
第二个检查也是通过的。
6个方法中,这2个就是最先执行的2个,
onAttachedToEngine()
先于 onAttachedToActivity()
,
但是不能获得 Activity 。
初始化地图 SDK 是不需要 Activity 的。需要的是 ApplicationContext 。
放在哪里都行,但是在 onAttachedToEngine()
获取 ApplicationContext 方便点。
初始化代码
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
// 在使用SDK各组件之前初始化context信息,传入ApplicationContext
SDKInitializer.initialize(flutterPluginBinding.getApplicationContext());
// 自4.3.0起,百度地图SDK所有接口均支持百度坐标和国测局坐标,用此方法设置您使用的坐标类型.
// 包括BD09LL和GCJ02两种坐标,默认是BD09LL坐标。
SDKInitializer.setCoordType(CoordType.BD09LL);
}
通过 FlutterPluginBinding 的 getApplicationContext()
就能得到 ApplicationContext 。
如果在 onAttachedToActivity()
初始化,
可以用 ActivityPluginBinding 的 getActivity() 得到 Activity 后,
在用 Activity 的 getApplicationContext()
得到 ApplicationContext 。
SDK 初始化后,创建一个用于显示地图的对象。
PlatformView
PlatformView 需要返回一个 View ,这个 View 是由百度地图 SDK 创建的。
查看百度地图文档,
地图容器分为 GLSurfaceView 和 TextureView 两种。
- GLSurfaceView
包括 MapView,MapFragment 和 SupportMapFragment 三种容器。- TextureView
包括 TexureMapView,TextureMapFragment 和 TextureSupportMapFragment 三种容器。
好像 TextureView 更好一点,我选择 TexureMapView 。
public class BaiduMapControler implements PlatformView {
private final TextureMapView mapView;
public BaiduMapControler(Activity activity) {
this.mapView = new TextureMapView(activity);
}
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
}
PlatformViewFactory
public class BaiduMapFactory extends PlatformViewFactory {
private final Activity activity;
public BaiduMapFactory(Activity activity) {
super(StandardMessageCodec.INSTANCE);
this.activity = activity;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
return new BaiduMapControler(activity);
}
}
登记 PlatformViewFactory
因为百度地图需要 Activity ,所以在 onAttachedToActivity()
中登记。
public class FlutterPluginDemoPlugin implements FlutterPlugin, ActivityAware {
// 为了在 onAttachedToActivity() 中得到 PlatformViewRegistry
private FlutterPluginBinding pluginBinding;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
pluginBinding = flutterPluginBinding;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
pluginBinding.getPlatformViewRegistry().registerViewFactory(
"baidu_map",
new BaiduMapFactory(binding.getActivity()));
}
}
在 Flutter 端显示百度地图
/lib/flutter_plugin_demo.dart
export 'src/baidu_map.dart';
/lib/src/baidu_map.dart
import 'package:flutter/material.dart';
class BaiduMapView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AndroidView(
viewType: "baidu_map",
);
}
}
/example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_plugin_demo/flutter_plugin_demo.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: BaiduMapView(),
),
),
);
}
}
运行后应该就可以看到地图了,如果只能看到格子,需要从百度获取开发密钥(AK)。
在百度地图官网文档中:
类 TextureMapView
使用这个类必须按照它的生命周期进行操控,你必须参照以下方法onCreate(Bundle)、 onResume()、onPause()、onDestroy()。等声明周期函数。在使用地图组件之前请确保已经调用了 SDKInitializer.initialize(Context) 函数以提供全局 Context 信息。
所以要想办法获取生命周期。
地图生命周期
和生命周期有关的方法在 Activity 中,要重写里面的方法好像有点不太方便。
向谷歌地图插件学习,让我们的 BaiduMapControler 再实现一个 DefaultLifecycleObserver 。
public class BaiduMapControler implements PlatformView, DefaultLifecycleObserver {
private final TextureMapView mapView;
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
}
}
多出的的方法正是和生命周期相关的。
DefaultLifecycleObserver 是什么呢?
DefaultLifecycleObserver
它可以作为 Activity 生命周期状态的观察者。
当 Activity 生命周期变化时,会调用观察者中相应的方法。
生命周期的观察者被保存在 Lifecycle 类中。
要观察某个 Activity 的生命周期,就要把观察者添加到 Activity 所拥有的 Lifecycle 中。
获取 Lifecycle
方法1
之前我们使用 ActivityPluginBinding 的 getActivity()
获取了 Activity 。
它也有个 getLifecycle()
方法用来获取 Lifecycle ,不过返回的类型是 Object 。
如果把它强制转换成 Lifecycle 。
会出现异常:
“io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference cannot be cast to androidx.lifecycle.Lifecycle”。
看下 HiddenLifecycleReference 的文档:
An Object that can be used to obtain a Lifecycle reference.
DO NOT USE THIS CLASS IN AN APP OR A PLUGIN.
This class is used by the flutter_android_lifecycle package to provide access to a Lifecycle in a way that makes it easier for Flutter and the Flutter plugin ecosystem to handle breaking changes in Lifecycle libraries.
特别强调了不让用。说是给“flutter_android_lifecycle”这个包用的。
谷歌地图插件好像就用了那个包。
看了那个包的代码,实现是很简单的。
所以可行的方法是这样:
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
//lifecycle = (Lifecycle) binding.getLifecycle();
HiddenLifecycleReference reference = (HiddenLifecycleReference) binding.getLifecycle();
lifecycle = reference.getLifecycle();
}
方法2
FlutterActivity 有个 getLifecycle()
方法是可以返回 Lifecycle 的。
我们传递给 BaiduMapControler 的 Activity ,其实就是 FlutterActivity 。
下面是使用方法2的例子。
最终的代码
public class BaiduMapControler implements PlatformView, DefaultLifecycleObserver {
private static final String TAG = "BaiduMapController";
private final TextureMapView mapView;
public BaiduMapControler(FlutterActivity activity) {
this.mapView = new TextureMapView(activity);
activity.getLifecycle().addObserver(this);
}
@Override
public View getView() {
return mapView;
}
@Override
public void dispose() {
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onCreate");
mapView.onCreate(null, null);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onStart");
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onResume");
mapView.onResume();
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onPause");
mapView.onPause();
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onStop");
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
Log.d(TAG, "onDestroy");
mapView.onDestroy();
}
}
参考资料
插件
- Developing packages & plugins
- Flutter之在Flutter布局中嵌入原生组件Android篇
-
Flutter 显示百度地图 Native 组件 -
chinesejar/flutter_with_map - 在Flutter中嵌入Native组件的正确姿势是...
纹理
- 万万没想到——flutter这样外接纹理
- Flutter 实时视频渲染:Texture与PlatformView
- 5分钟彻底搞懂Flutter中PlatFormView与Texture
- Flutter浪潮下的音视频研发探索