Flutter 插件 百度地图Android插件演示

2020-05-13 17:33:34
Flutter,百度地图,原生插件

简介

先介绍一下,这个插件只是教学目的,并不完整。
目前提供了

  • 显示地图
  • 获取地图中心坐标
  • 定位
  • 距离计算

写这个插件的时候,
我只学了 Flutter 一个月,Android 没学过,也没掌握 Java 。

项目地址

没想到文章越写越多,分成了3部分,这是最后一部分,
前2部分链接:

地图插件的关键是 Flutter 端的 AndroidView 和 Android 端的 PlatformView 。

Flutter 如何显示 Android 视图

要想让 Flutter 显示一个安卓视图 。

  1. 安卓端需要提供一个 PlatformView 来生成安卓视图,即 View
  2. PlatformView 需要用 PlatformViewFactory 来创建。
  3. 最后通过 PlatformViewRegistry 登记 PlatformViewFactory 。
  4. 在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 ,
需要用到 PlatformViewRegistryregisterViewFactory() 方法。

PlatformViewRegistry 是在应用启动后,创建 FlutterEngine 时创建的。
怎样得到它?

  • 如果我们可以直接得到 FlutterEngine 。
    例如在 FlutterActivity 的 configureFlutterEngine() 方法中。
    那么可以先用 FlutterEngine 的 getPlatformViewsController() 方法
    得到 PlatformViewsController
    然后用 PlatformViewsController 的 getRegistry() 得到 PlatformViewRegistry 。

  • 如果我们在写插件,可以在 onAttachedToEngine()
    通过 FlutterPluginBindinggetPlatformViewRegistry() 获得。

PlatformViewRegistry 的 registerViewFactory() 方法接受2个参数,

  • 第一个是 viewType ,字符串类型。是一个唯一的标识符,用来标识一个 PlatformViewFactory 。
  • 第二个是 PlatformViewFactory 。用来创建 PlatformView 。

它有个 boolean 类型的返回值。
登记的时候会通过 viewType 检查是否已经登记过,
如果登记过返回 false,否则返回 true 。

PlatformViewFactory

创建 PlatformViewFactory 需要指定一个解码器,用于解码 Flutter 端传递过来的参数。
这个解码器要与 Flutter 端的编码器对应,一般是 StandardMessageCodec

我们需要重写 create() 方法。
有3个可用的参数:

  1. context
    最开始是个 FlutterActivity ,经过层层包装,最后是一个 ContextWrapper 。
    包含了 InputMethodManager ,InvocationHandler,WindowManager, 重写了 getSystemService() ,
    这些我都不懂。
  2. viewId
    Flutter 端传递过来的 id 。
  3. 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 类所在的文件。

示例插件实现了 FlutterPluginMethodCallHandler
之所以实现 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

之前我们使用 ActivityPluginBindinggetActivity() 获取了 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();
  }
}

参考资料

插件

纹理

Android 生命周期

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