來一手Flutter Web =-= 實現高德地圖外掛

一隻程式碼猴發表於2020-06-09
現組建了一個flutter的水友圈,有Android的混合開發及flutter分析,還有Dart語法詳解等資料;可以從初始flutter到flutter進階視訊學習,包括flutter優化及開源實戰,文末有進圈方式

1.調侃一下

以前寫了一個功能簡單的高德地圖外掛,當時支援了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的庫名。
來一手Flutter Web =-= 實現高德地圖外掛
如果我們要實現上圖的呼叫,就需要接著定義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的文件
來一手Flutter Web =-= 實現高德地圖外掛
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 物件,這個類的文件如下:
來一手Flutter Web =-= 實現高德地圖外掛
所以對應的Dart封裝如下:
@JS()
class LngLat {
  external num getLng();
  external num getLat();
  external LngLat(num lng, num lat);
}複製程式碼
這裡我沒有寫完全,只提供了我用到的getLng、getLat方法。
這裡我們使用一下我們目前的成果:
JS程式碼:
來一手Flutter Web =-= 實現高德地圖外掛
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.回撥

也就是傳遞函式,這裡以地圖外掛載入方法來舉例。文件如下:
來一手Flutter Web =-= 實現高德地圖外掛
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來進行載入地圖及外掛。使用方法如下:
來一手Flutter Web =-= 實現高德地圖外掛
這部分程式碼的封裝很簡單:
@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複製程式碼
執行後發現這些問題得到了解決,但是又產生了新的問題。比如地圖不能點選、拖動,文字亂碼。。。
來一手Flutter Web =-= 實現高德地圖外掛
當然官方的文章也指出,現階段CanvasKit引擎還是比較粗糙的,而DomCanvas引擎相對更加穩定。 實現的功能:
  • 自動定位並根據當前經緯度進行POI搜尋
  • 點選地圖獲取經緯度並進行POI搜尋
  • 點選地址資訊,移動地圖至當前位置
  • POI搜尋功能

結語

歡迎大家入圈一起來實戰flutter開源,更多資料請戳
Android學習、面試;文件、視訊資源免費獲取​
https://shimo.im/docs/W86YcTtpQ3pYycdd/ 《Android學習、面試;文件、視訊資源免費獲取》,可複製連結後用石墨文件 App 或小程式開啟
或者私信我資料領取
來一手Flutter Web =-= 實現高德地圖外掛

相關文章