去年写了一个功能简单的高德地图插件给flutter_deer使用,当时支持了Android与iOS两端。前一阵子有一个issue问是否会支持Flutter Web,当时我有点懵,毕竟js我都不熟。。。不过先记下这个需求,等着有时间了去研究一下。javascript
过了一个月,忽然想起了这件事。就先去搜索了一下相关资料,发现都是实现的谷歌地图。而这些都使用到了一个google_maps的开源库。这个库其实就是借助js_wrapping封装了谷歌地图的js库,达到使用Dart代码调用js代码的目的。html
没办法了,看来我也只能去封装高德地图的js了。本想着照葫芦画瓢使用js_wrapping
去实现,后面发现Dart sdk有提供操做js api的dart:js
,同时也提供了更易于使用的package:js。java
这部分我尽可能说的细一点,毕竟目前相关资料很少(还不快收藏起来~~)。避免你们像我一开始同样一头雾水。下面就以高德地图的Api来举例说明如何实现Dart调用JS代码,git
首先在pubspec.yaml
添加依赖:github
dependencies:
# https://pub.flutter-io.cn/packages/js#-readme-tab-
js: ^0.6.1+1
复制代码
建立amapjs.dart
文件,导入package:js
,同时用@JS
注解指定库名:web
@JS('AMap')
library amap;
import 'package:js/js.dart';
复制代码
这里的AMap
实际就是高德js的库名。chrome
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来化解问题:api
@JS('Map')
class AMap {
external AMap(String id);
}
复制代码
而添加external
关键字的意思是指“外在”,也就是说这个方法是js代码实现的。数组
下面咱们看一下Map的文档: promise
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
对象,这个类的文档以下:
@JS()
class LngLat {
external num getLng();
external num getLat();
external LngLat(num lng, num lat);
}
复制代码
这里我没有写彻底,只提供了我用到的getLng
、getLat
方法。
这里咱们使用一下咱们目前的成果:
JS代码:
MapOptions _mapOptions = MapOptions(
zoom: 11,
viewMode: '3D',
center: LngLat(116.397428, 39.90923),
);
AMap aMap = AMap('container', _mapOptions);
复制代码
到这里咱们也能发现,大多数的基础类型咱们都是能够和js去一一对应上的,好比我用到的String、num、bool、List,对于Map类型须要咱们本身封装。
来自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>
>,使用时再转换或强转。
也就是传递函数,这里以地图插件加载方法来举例。文档以下:
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
方法确保兼容性。
举例:高德推荐使用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');
});
复制代码
使用上面的方法,我将我使用到的高德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的搜索、地图点击这类不涉及显示的功能,测试均是正常的,基本能够知足使用。放一下现阶段的效果展现:
其实从Flutter Web去年的首个预览版、到去年末的Beta版、到如今。我都会将flutter_deer在web端运行一下,我能明显的感受到它表现的愈来愈好了,好比有时文字不居中、动画表现不一致、Stack
的层级显示不正确等许多小问题都获得了解决。说不定哪天我再次运行起来,上面说的问题都解决了,哈哈!!
此次发现Flutter Web也支持了PWA
,我在PC和手机体验下来发现是真的很不错~~,手机端看上去几乎能够以假乱真,其实上面的GIF就是PC端的效果。
最后,我将这部分完整代码都已提交至Github,如今这个小插件已经支持Android、iOS和Web了,欢迎体验!!最最后,点赞支持一下~