百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

玩玩Flutter Web——实现高德地图插件

wptr33 2024-12-05 17:00 13 浏览

1.啰嗦几句

去年写了一个功能简单的高德地图插件给flutter_deer使用,当时支持了Android与iOS两端。前一阵子有一个issue问是否会支持Flutter Web,当时我有点懵,毕竟js我都不熟。。。不过先记下这个需求,等着有时间了去研究一下。

过了一个月,突然想起了这件事。就先去搜索了一下相关资料,发现都是实现的谷歌地图。而这些都使用到了一个google_maps的开源库。这个库其实就是借助js_wrapping封装了谷歌地图的js库,达到使用Dart代码调用js代码的目的。

没办法了,看来我也只能去封装高德地图的js了。本想着照葫芦画瓢使用js_wrapping去实现,后面发现Dart sdk有提供操作js api的dart:js,同时也提供了更易于使用的package:js。

2.Dart调用JS

这部分我尽量说的细一点,毕竟目前相关资料不多(还不快收藏起来~~)。避免大家像我一开始一样一头雾水。下面就以高德地图的Api来举例说明如何实现Dart调用JS代码,

首先在pubspec.yaml添加依赖:

dependencies:
  #  https://pub.flutter-io.cn/packages/js#-readme-tab-
  js: ^0.6.1+1

创建amapjs.dart文件,导入package:js,同时用@JS注解指定库名:

@JS('AMap')
library amap;

import 'package:js/js.dart';

这里的AMap实际就是高德js的库名。

如果我们要实现上图的调用,就需要接着定义Map对象:

@JS('AMap')
library amap;

import 'package:js/js.dart';

// 这里`new Map(id)` 调用js的`new AMap.Map(id)`
@JS()
class Map {
  external Map(String id);
}

这里如果直接调用Map 可能会和Map<K, V>产生歧义,所以我们可以给注解@JS指定name来化解问题:

@JS('Map')
class AMap {
  external AMap(String id);
}

而添加external关键字的意思是指“外在”,也就是说这个方法是js代码实现的。

下面我们看一下Map的文档:

Map的构造方法不止一个div id这么简单,也可能是HTMLDivElement,所以我们不能使用之前的String类型了。同时有MapOptions这个初始化的参数对象。

@JS('Map')
class AMap {
  external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
}

而MapOptions实际是一个Map<K, V>结构,并不是一个类,所以我们需要添加@anonymous注解,否则创建MapOptions 就成了new AMap.MapOptions ,这个显然不在js库中。

@JS()
@anonymous
class MapOptions {
  external factory MapOptions({
    /// 初始中心经纬度
    LngLat center,
    /// 地图显示的缩放级别
    num zoom,
    /// 地图视图模式, 默认为‘2D’
    String /*‘2D’|‘3D’*/ viewMode,
  });
}

如果你想获取或修改某些参数,可以添加对应的get、set方法。

@JS()
@anonymous
class MapOptions {
  external LngLat get center;
  external set center(LngLat v);
  external factory MapOptions({
    LngLat center,
    num zoom,
    String /*‘2D’|‘3D’*/ viewMode,
  });
}

MapOptions 的代码中出现了LngLat 对象,这个类的文档如下:

所以对应的Dart封装如下:

@JS()
class LngLat {
  external num getLng();
  external num getLat();
  external LngLat(num lng, num lat);
}

这里我没有写完全,只提供了我用到的getLng、getLat方法。


这里我们使用一下我们目前的成果:

JS代码:

Dart代码:

MapOptions _mapOptions = MapOptions(
  zoom: 11,
  viewMode: '3D',
  center: LngLat(116.397428, 39.90923),
);
AMap aMap = AMap('container', _mapOptions);

到这里我们也能发现,大多数的基础类型我们都是可以和js去一一对应上的,比如我用到的String、num、bool、List,对于Map类型需要我们自己封装。

3.进阶

1.List

来自JavaScript的数组实例总是List<dynamic>
JavaScript数组没有具体的元素类型,因此JavaScript函数返回的数组不能在不检查每个元素的情况下保证其元素类型。

举个例子:假设js有个数组list = ['Android', 'iOS', 'Web'];,看似以为它是个List<String>,其实它是List<dynamic>。

// true
print(list is List);
// false
print(list is List<String>);

在高德里有个poi的搜索功能,最后会返回一个Array<Poi>,实现代码如下:

@JS()
@anonymous
class PoiList {
  external List<dynamic> get pois;
}

@JS()
@anonymous
class Poi {
  external String get citycode;
  external String get cityname;
  external String get adname;
  external String get name;
  ...
}

// 使用时
pois.forEach((poi) {
  if (poi is Poi) {
       poi.citycode;
       ...
  }
});

这里的List我尝试过使用List<Poi>,测试也没什么问题。但是pois确实返回的是List<dynamic>,所以稳妥的写法还是使用List<dynamic>>,使用时再转换或强转。

2.回调

也就是传递函数,这里以地图插件加载方法来举例。文档如下:

JS代码如下:

mapObj.plugin(["AMap.ToolBar"], function() {
    //加载工具条
    var tool = new AMap.ToolBar();
    mapObj.addControl(tool);
});

其实这里的function就对应Dart的Function:

@JS('Map')
class AMap {
  external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);
  /// 加载插件
  external plugin(dynamic/*String|List*/ name, void Function() callback); 
}

如果function有参数也是一样的。唯一的区别在于使用时的不同:

import 'package:js/js.dart';
// 错误
mapObj.plugin(['AMap.ToolBar'], () {
  mapObj.addControl(ToolBar());
});
// 正确
mapObj.plugin(['AMap.ToolBar'], allowInterop(() {
  mapObj.addControl(ToolBar());
}));

如果将Dart函数作为参数传递给JS Api,则需要使用allowInterop或allowInteropCaptureThis方法确保兼容性。

3.异步

举例:高德推荐使用JSAPI Loader来进行加载地图及插件。使用方法如下:

这部分代码的封装很简单:

@JS('AMapLoader')
library loader;

import 'package:js/js.dart';

/// 高德地图 Loader js
external load(LoaderOptions options);

@JS()
@anonymous
class LoaderOptions {

  external factory LoaderOptions({
    ///您申请的key值
    String key,
    /// JSAPI 版本号
    String version,
    //同步加载的插件列表
    List<String> plugins,
  });
}

主要还是使用上,怎么将Js的Promise转换成Dart的Future 。这里就用到了promiseToFuture方法,源码如下:

Future<T> promiseToFuture<T>(jsPromise) {
  final completer = Completer<T>();

  final success = convertDartClosureToJS((r) => completer.complete(r), 1);
  final error = convertDartClosureToJS((e) => completer.completeError(e), 1);

  JS('', '#.then(#, #)', jsPromise, success, error);
  return completer.future;
}

使用代码示例:

import 'dart:js_util';

var promise = load(LoaderOptions(
  key: 'xxx',
  version: '2.0',
  plugins: ['AMap.Scale'],
));

promiseToFuture(promise).then((value) {
  AMap aMap = AMap('container');
  ...      
}, onError: (e) {
  print('初始化错误:$e');
});

4.显示地图

使用上面的方法,我将我使用到的高德api进行了封装,完成了JS调用部分的工作。到这里就剩下了地图显示及相应的逻辑实现了。

功能的逻辑实现这里就不多说了,主要说说如何显示地图。

首先在web目录的index.html中添加js(在main.dart.js之前):

<script src="https://webapi.amap.com/loader.js"></script>

其实与Android的AndroidView和iOS的UiKitView相同,Web这边有个HtmlElementView。(Flutter sdk:Dev channel 1.19.0-1.0.pre)

它需要一个由PlatformViewFactory注册的唯一标识符viewType。

/// 这里使用时间作为唯一标识
_divId = DateTime.now().toIso8601String();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) => HtmlElement());

return HtmlElementView(
  viewType: _divId,
);

地图创建需要div的id或者HTMLDivElement,所以我们需要创建一个div。在Dart的dart:html中为我们提供了DOM element、CSS样式、本地存储、音视频、事件等(4万行代码不是盖的...)。其中就有这里需要的HTMLDivElement:

@Native("HTMLDivElement")
class DivElement extends HtmlElement {
  // To suppress missing implicit constructor warnings.
  factory DivElement._() {
    throw new UnsupportedError("Not supported");
  }

  factory DivElement() => JS('returns:DivElement;creates:DivElement;new:true',
      '#.createElement(#)', document, "div");
  /**
   * Constructor instantiated by the DOM when a custom element has been created.
   *
   * This can only be called by subclasses from their created constructor.
   */
  DivElement.created() : super.created();
}

整理后,完整代码如下:

import 'dart:html';
import 'dart:ui' as ui;

String _divId;
DivElement _element;

@override
void initState() {
  super.initState();
  /// 这里使用时间作为唯一标识
  _divId = DateTime.now().toIso8601String();
  /// 先创建div并注册
  // ignore: undefined_prefixed_name
  ui.platformViewRegistry.registerViewFactory(_divId, (int viewId) {
    /// 地图需要的Div
    _element = DivElement()
      ..style.width = '100%'
      ..style.height = '100%'
      ..style.margin = '0';

    return _element;
  });
  SchedulerBinding.instance.addPostFrameCallback((_) {
    /// 创建地图
    var promise = load(LoaderOptions(
      key: 'xxx',
      version: '2.0',
      plugins: ['AMap.Scale'],
    ));

    promiseToFuture(promise).then((value) {
      AMap aMap = AMap(_element);
    }, onError: (e) {
      print('初始化错误:$e');
    });
  });
}

@override
Widget build(BuildContext context) {
  return HtmlElementView(
    viewType: _divId,
  );
}

这里其实有点问题,HtmlElementView没有和AndroidView、UiKitView一样给予onCreatePlatformView创建回调,导致我直接创建的地图会显示不出来,所以我使用了addPostFrameCallback来处理。或者参考这个issue,自定义PlatformViewLink来实现。

不过我遇见的问题不止这些,大都是地图的显示问题。比如:

  • 高德地图的logo、定位、比例尺这类不显示,部分在地图的左上角被地图层覆盖。
  • 地图上的覆盖物添加后无法修改。
  • 地图上的覆盖物在地图放大缩小后位置偏离。(和第二点类似)

可以看出问题都是渲染上的,查询相关资料得知现在是基于HTML DOM的模型,该模型结合了HTML,CSS和Canvas API来实现页面,官方将此实现称为DomCanvas渲染系统。而目前在尝试使用第二种方法CanvasKit,CanvasKit 使用WebAssembly和WebGL将Skia引入Web,利用硬件加速从而提高了渲染复杂和密集图形的能力。

现阶段Flutter Web 默认使用DomCanvas,所以我尝试使用以下命令启用CanvasKit渲染引擎来看看效果:

flutter run -d chrome --release --dart-define=FLUTTER_WEB_USE_SKIA=true

运行后发现这些问题得到了解决,但是又产生了新的问题。比如地图不能点击、拖动,文字乱码。。。

当然官方的文章也指出,现阶段CanvasKit引擎还是比较粗糙的,而DomCanvas引擎相对更加稳定。

最终我还是使用了稳定的方案。因为其他功能,比如poi的搜索、地图点击这类不涉及显示的功能,测试均是正常的,基本可以满足使用。

实现的功能:

  • 自动定位并根据当前经纬度进行POI搜索
  • 点击地图获取经纬度并进行POI搜索
  • 点击地址信息,移动地图至当前位置
  • POI搜索功能

其实从Flutter Web去年的首个预览版、到去年底的Beta版、到现在。我都会将flutter_deer在web端运行一下,我能明显的感觉到它表现的越来越好了,比如有时文字不居中、动画表现不一致、Stack的层级显示不正确等许多小问题都得到了解决。说不定哪天我再次运行起来,上面说的问题都解决了,哈哈!!

这次发现Flutter Web也支持了PWA,我在PC和手机体验下来发现是真的很不错~~,手机端看上去几乎可以以假乱真,其实上面的GIF就是PC端的效果。

最后,我将这部分完整代码(https://github.com/simplezhli/flutter_2d_amap)都已提交至Github,现在这个小插件已经支持Android、iOS和Web了,欢迎体验!!最最后,点赞支持一下~

相关推荐

C++企业级开发规范指南(c++开发gui)

打造高质量、可维护的C++代码标准一、前言C++作为一门功能强大的系统级编程语言,被广泛应用于操作系统、游戏引擎、高性能服务器、数据库系统等领域。知名互联网公司(如Google、Microsoft、腾...

C++|整型的最值、上溢、下溢、截断、类型提升和转换

整数在计算机内以有限字长表示,当超出最值(有限字长)时,需要截断(溢出,求模)操作。不同字长的整型具有不同的值域,混合运算时,需要类型提升和转换。1整形最值在<limit.h>中有整型的...

C++|漫谈STL细节及内部原理(c++ std stl)

1988年,AlexanderStepanov开始进入惠普的PaloAlto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任BillWo...

C++11新特性总结 (二)(c++11新特性 pdf)

1.范围for语句C++11引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素vector<int>vec={1,2,3,4,5,6};f...

C++ STL 漫谈(c++中的stl到底指的什么)

标准模板库(StandardTemplateLibrary,STL)是惠普实验室开发的一个函数库和类库。它是由AlexanderStepanov、MengLee和DavidRMusser在...

C++学习教程_C++语言随到随学_不耽误上班_0基础

C++学习教程0基础学C++也可以,空闲时间学习,不耽误上班.2019年C语言新课程已经上线,随到随学,互动性强,效果好!带你征服C++语言,让所有学过和没有学过C++语言的人,或是正准备学习C++语...

C++遍历vector元素的四种方式(c++ 遍历vector)

vector是相同类型对象的集合,集合中的每个对象有个对应的索引。vector常被称为容器(container)。C++中遍历vector的所有元素是相当常用的操作,这里介绍四种方式。1、通过下标访问...

一起学习c++11——c++11中的新增的容器

c++11新增的容器1:array当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用stl中的模板算法。array的用法如下:#include<string>#includ...

C++编程实战基础篇:一维数组应用之投票统计

题目描述班上有N个同学,有五位候选人“A,B,C,D,E”,请所有的同学投票并选举出班长,现在请你编写程序来他们计算候选人的得票总数,每位同学投票将以数字的形式投票“12345”分别代表五位候选人,...

C++20 新特性(6):new表达式也支持数组大小推导

new表达式也支持数组大小推导在C++17标准中,在定义并初始化静态数组时,是可以忽略数组大小,然后通过初始化数据来推导数组的大小。但使用new来定义并初始化动态数组时,并不支持这种自动推导数组大...

C++ 结构体(struct)最全详解(c++结构体用法)

一、定义与声明1.先定义结构体类型再单独进行变量定义structStudent{intCode;charName[20];charSex;intA...

自学 C++ 第 6 课 二维数组找最值

键盘输入一个m×n的二维数组,通过C++编程找出元素中的最大值,并输出其所在的位置坐标。例如,输入一个4×5的二维数组,数组元素分别为{{556623749},{578964563},...

从缺陷中学习C/C++:聊聊 C++ 中常见的内存问题

在写C/C++程序时,一提到内存,大多数人会想到内存泄露。内存泄露是一个令人头疼的问题,尤其在开发大的软件系统时。一个经典的现象是,系统运行了10天、1个月都好好的,忽然有一天宕机了:OOM(Out...

C++开发者都应该使用的十个C++11特性(上)

在C++11新标准中,语言本身和标准库都增加了很多新内容,本文只涉及了一些皮毛。不过我相信这些新特性当中有一些,应该成为所有C++开发者的常规装备。你也许看到过许多类似介绍各种C++11特性的文章。下...

深度解读C/C++指针与数组(c++指针和数组的区别)

指针和数组是密切相关的。事实上,指针和数组在很多情况下是可以互换的。例如,一个指向数组开头的指针,可以通过使用指针的算术运算或数组索引来访问数组。今天我们就来聊一聊数组和指针千丝万缕的关系;一维数组与...