玩玩Flutter Web —— 實現高德地圖插件

Red Deer

1.囉嗦幾句

去年寫了一個功能簡單的高德地圖插件給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:jsjava

2.Dart調用JS

這部分我儘可能說的細一點,畢竟目前相關資料很少(還不快收藏起來~~)。避免你們像我一開始同樣一頭霧水。下面就以高德地圖的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的文檔
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, });
}

複製代碼

若是你想獲取或修改某些參數,能夠添加對應的getset方法。

@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);
}
複製代碼

這裏我沒有寫徹底,只提供了我用到的getLnggetLat方法。


這裏咱們使用一下咱們目前的成果:

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.回調

也就是傳遞函數,這裏以地圖插件加載方法來舉例。文檔以下:

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,則須要使用allowInteropallowInteropCaptureThis方法確保兼容性。

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<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沒有和AndroidViewUiKitView同樣給予onCreatePlatformView建立回調,致使我直接建立的地圖會顯示不出來,因此我使用了addPostFrameCallback來處理。或者參考這個issue,自定義PlatformViewLink來實現。

不過我碰見的問題不止這些,大都是地圖的顯示問題。好比:

  • 高德地圖的logo、定位、比例尺這類不顯示,部分在地圖的左上角被地圖層覆蓋。

  • 地圖上的覆蓋物添加後沒法修改。

  • 地圖上的覆蓋物在地圖放大縮小後位置偏離。(和第二點相似)

能夠看出問題都是渲染上的,查詢相關資料得知如今是基於HTML DOM的模型,該模型結合了HTMLCSSCanvas API來實現頁面,官方將此實現稱爲DomCanvas渲染系統。而目前在嘗試使用第二種方法CanvasKitCanvasKit 使用WebAssemblyWebGLSkia引入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.參考

相關文章
相關標籤/搜索