Red Deer

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的库名。

[图片上传失败...(image-10b02e-1590974034090)]

如果我们要实现上图的调用,就需要接着定义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产生歧义,所以我们可以给注解@JS指定name来化解问题:

@JS('Map')

class AMap {

external AMap(String id);

}

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

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

Map的文档

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

@JS('Map')

class AMap {

external AMap(dynamic /*String|HTMLDivElement*/ div, MapOptions opts);

}

而MapOptions实际是一个Map结构,并不是一个类,所以我们需要添加@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对象,这个类的文档如下:

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

JavaScript数组没有具体的元素类型,因此JavaScript函数返回的数组不能在不检查每个元素的情况下保证其元素类型。

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

// true

print(list is List);

// false

print(list is List);

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

@JS()

@anonymous

class PoiList {

external List 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,测试也没什么问题。但是pois确实返回的是List,所以稳妥的写法还是使用List>,使用时再转换或强转。

2.回调

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

plugin方法文档

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来进行加载地图及插件。使用方法如下:

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 plugins,

});

}

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

Future promiseToFuture(jsPromise) {

final completer = Completer();

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之前):

其实与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版效果

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

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

效果展示

实现的功能:

自动定位并根据当前经纬度进行POI搜索

点击地图获取经纬度并进行POI搜索

点击地址信息,移动地图至当前位置

POI搜索功能

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

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

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

5.参考

flutter引入高德地图_玩玩Flutter Web —— 实现高德地图插件相关推荐

  1. flutter图片聊天泡泡_基于 Flutter+Dart 聊天实例 | Flutter 仿微信界面聊天室

    1.项目介绍 Flutter是目前比较流行的跨平台开发技术,凭借其出色的性能获得很多前端技术爱好者的关注,比如阿里闲鱼,美团,腾讯等大公司都有投入相关案例生产使用. flutter_chatroom项 ...

  2. java实现地图导航功能吗_Java web实现百度地图导航

    接下来要注册成为百度地图个人开发者才能使用百度地图 填写相关的资料,我已经认证成功,资料填写很简单 由于百度地图要基于相应的应用,所以必须创建一个应用才可以使用秘钥,要不然地图不能打开.应用名称随便输 ...

  3. 我的世界java版怎么安装地图_我的世界手机版地图怎么安装 地图安装教程分享...

    我的世界手机版地图很多,那么下载的地图怎么安装呢?本文为大家带来我的世界手机版地图安装教程分享,希望能帮到各位. 地图安装教程 1.找到我的世界安装目录 首先玩家需要先找到地图存档路径,路径如上图.游 ...

  4. python三维图形渲染 地图_原来炫酷的可视化地图,用Python就能搞定!

    对于可视化地图,Pyecharts是小F以前用的最多的. 还有应该就是一些网站了,现成的工具,不需要敲代码. 在5G手机可视化那一篇文章,也是用Pyecharts做的地图- 而对于Matplotlib ...

  5. web高德地图怎么加载离线地图_怎么验证全国离线卫星地图缓存文件的完整性

    1. 概述 自从我们发布了<如何离线浏览全国7137GB海量谷歌卫星离线影像>一文后,很多用户都在问下载海量数据的具体方法,为了解决大家的困惑,于是又分别发布了<如何一次性下载全国谷 ...

  6. python爬高德地图_利用Python爬取高德地图数据

    准备1.高德开放平台注册账户 https://lbs.amap.com/dev/index 验证手机号码.邮箱后进入开发者后台创建一个应用: 并为该应用添加 Key,服务平台选择 web 服务 申请完 ...

  7. python爬取高德数据_利用Python爬取高德地图数据

    准备1.高德开放平台注册账户 https://lbs.amap.com/dev/index 验证手机号码.邮箱后进入开发者后台创建一个应用: 并为该应用添加 Key,服务平台选择 web 服务 申请完 ...

  8. ios 持续获取定位 高德地图_概述-iOS 定位SDK | 高德地图API

    简介 高德 iOS 定位 SDK 提供了不依赖于地图定位的定位功能,开发者可以无地图显示的场景中便捷地为应用程序添加定位功能. iOS定位SDK提供了单次定位.连续定位.逆地理信息.地理围栏等功能. ...

  9. flutter 拖拽布局_使用Flutter模仿SOUL可任意拖动的悬浮按钮

    在Flutter中,我们可以两种方式实现小部件对拖动效果 GestureDetector() Draggable() GestureDetector 在开发过程中经常使用到GestureDetecto ...

最新文章

  1. 通过 Keras 构建深度学习模型的步骤
  2. 为什么博士要挤破头进高校?同一个实验室,硕士们进了大厂拿高薪
  3. python程序实例教程基础-Python程序设计案例教程——从入门到机器学习(微课版)...
  4. Linux Tomcat 简介
  5. linux c之用fwrite和fread实现文件的复制
  6. query row php,php – 如何在Codeigniter上使用$query- row获取类对象
  7. 云服务器,Linux上安装xampp,搭建node服务,云服务器Firewalld、iptables,nginx反向代理
  8. 可逆与不可逆算法,对称加密与非对称加密算法总结
  9. c++ 数字后加f_Pandas 实战系列:数字格式设置
  10. 八选一数据选择器(两种写法)
  11. tplink无线网卡的linux,ubuntu上tp-link无线网卡驱动安装
  12. Python零基础福利来了!
  13. setCookie时遇到的问题
  14. BC20+MQTT+OneNet 订阅主题总是error
  15. (附源码)ssm招聘网站 毕业设计 250858
  16. docker 搭建本地 coredns 服务器
  17. “三高“Mysql - Mysql备份概览
  18. 2021-10-18 WPF调用dll出现异常
  19. JS经典案例-无缝滚动轮播图(纯JS)
  20. CC254x简介-CC2541/CC2540

热门文章

  1. 一颗椰子糖机器人_“媒体训练营”走进海南春光 聆听椰子糖诞生故事
  2. Gridmanager
  3. 次短路问题(陕西师范大学第九届ACM程序设计竞赛-F 新冠病毒要回家)
  4. php中empty功能,深入理解PHP中的 empty()函数
  5. VC6代码移植VS2008的SDK总结,包括OPENGL和DireX还有Cimage 【总结】
  6. 三元运算符的基本用法
  7. 2022.02.23_HTML+CSS学习总结_CSS初识、选择器与标签的分类
  8. 尝试做个公众号,歌曲类型的,有兴趣可以关注一下
  9. Java中字符与字节常识
  10. 邬江兴-拟态计算与拟态安全