关于在Flutter实现Google地图的方法
由于项目需要开发一款国际化APP(多语种)。公司是一家国内的车联网企业,车辆地图监控为项目的基础功能模块。在国内项目用的是高德,高德国际化支持太差劲,在国际化中直接被PASS掉了。为了能达到全球化的位置监控展示,尝试过MapBox,最后考虑种种还是使用Google地图。
话说,Google在国内的现状目前是啥样我想大家也都清楚,在9月份的某一天突然发现国内的Google地图网页也被重定向google.cn。看来这是真的打算撤退的节奏啊。偶然间在网上看到说 用电脑访问 http://www.google.cn//maps 可以访问,真的神奇了。
虽然Google国内地图被关闭了,但是好在API可以正常使用。
项目是开发一款基于IOS/Android的移动端软件,在调研前期选择了Flutter去开发,因为看到Flutter对IOS/Android的支持还是不错的,一套代码编译为两个平台的软件。
当然,Google Map 官方也当然提供的有Flutter 的插件 google_maps_flutter,开箱即用很是方便。直接上手。
基于google_maps_flutter的开发准备工作:
1、首先需要Google地图的API Key
2、手机需要Google 服务全家桶(不懂请自行度娘喔~ )
添加依赖:
dependencies:
google_maps_flutter: ^0.5.21+12
插件的使用方法在【https://pub.dev/packages/google_maps_flutter】中也介绍的很详细。直接copy代码走起,如下~
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Google Maps Demo',
home: MapSample(),
);
}
}
class MapSample extends StatefulWidget {
@override
State<MapSample> createState() => MapSampleState();
}
class MapSampleState extends State<MapSample> {
Completer<GoogleMapController> _controller = Completer();
static final CameraPosition _kGooglePlex = CameraPosition(
target: LatLng(37.42796133580664, -122.085749655962),
zoom: 14.4746,
);
static final CameraPosition _kLake = CameraPosition(
bearing: 192.8334901395799,
target: LatLng(37.43296265331129, -122.08832357078792),
tilt: 59.440717697143555,
zoom: 19.151926040649414);
@override
Widget build(BuildContext context) {
return new Scaffold(
body: GoogleMap(
mapType: MapType.hybrid,
initialCameraPosition: _kGooglePlex,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _goToTheLake,
label: Text('To the lake!'),
icon: Icon(Icons.directions_boat),
),
);
}
Future<void> _goToTheLake() async {
final GoogleMapController controller = await _controller.future;
controller.animateCamera(CameraUpdate.newCameraPosition(_kLake));
}
}
在运行代码之前需要配置API Key :
Android:
在android/app/src/main/AndroidManifest.xml中配置:
<application>
...
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="您的Key"/>
</application>
IOS :
在Info.plist中配置如下:
<dict>
......
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict>
两种配置Key的方法:
方法一:
在ios/Runner/AppDelegate.m中配置Key:
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#import "GoogleMaps/GoogleMaps.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GMSServices provideAPIKey:@"您的Key"];
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
方法二:
在ios/Runner/AppDelegate.swift中配置Key
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("您的Key")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
OK,开始运行。
嗯~???? 设备不支持?我的模拟器没有安装Google 服务全家桶。目前国内的Android手机都已经将Google内置服务去掉了。然而Google地图在Android却需要Google服务的支持。这就有些尴尬了。总不可能以后凡是使用该APP的用户都强迫安装这个吧?客户首先就不买单。
于是开始第二种尝试,用HTML实现地图并嵌套在APP中。
方案实现思路:使用webview嵌套一个页面,在页面中展示Google地图。通过页面JS回调Flutter里面的方法;Flutter通过webview_flutter插件提供的evaluateJavascript函数调用页面上的方法操作页面地图(比如地图缩放、地图绘制、地图查找)
准备工作:
1、首先需要Google地图的API Key
2、依赖
dependencies:
webview_flutter: ^0.3.15+1
代码:
class Lnglat {
double lng;
double lat;
Lnglat(this.lng, this.lat);
@override
String toString() {
return 'Lnglat{lng: $lng, lat: $lat}';
}
Map<String, dynamic> toJson() {
Map<String, dynamic> _m = new Map<String, dynamic>();
_m["lng"] = this.lng;
_m["lat"] = this.lat;
return _m;
}
factory Lnglat.fromJson(Map<String, dynamic> json) {
return Lnglat(
json['lng'],
json['lat']);
}
}
class InfoWindow {
String content;
///信息的最大宽度,与内容的宽度无关。仅当在调用open之前设置此值时,才会考虑此值。
///若要在更改内容时更改最大宽度,请调用close、setOptions,然后调用open。
double maxWidth;
///所有InfoWindows都按其zIndex的顺序显示在地图上,较高的值显示在较低值的InfoWindows前面。
///默认情况下,InfoWindows根据纬度显示,较低纬度的InfoWindows出现在较高纬度的InfoWindows前面。
///信息窗口始终显示在标记前面。
int zIndex;
InfoWindow({this.content, this.maxWidth, this.zIndex});
@override
String toString() {
return 'InfoWindow{content: $content, maxWidth: $maxWidth, zIndex: $zIndex}';
}
Map<String, dynamic> toJson() {
Map<String, dynamic> _m = new Map<String, dynamic>();
_m["content"] = this.content;
_m["maxWidth"] = this.maxWidth;
_m["zIndex"] = this.zIndex;
return _m;
}
factory InfoWindow.fromJson(Map<String, dynamic> json) {
return InfoWindow(
content: json['content'],
maxWidth: json['maxWidth'],
zIndex: json['zIndex']);
}
}
import 'InfoWindow.dart';
import 'Lnglat.dart';
class Marker {
/// 点标记在地图上显示的位置,默认为地图中心点
Lnglat position;
/// 需在点标记中显示的图标
String icon;
/// 鼠标滑过点标记时的文字提示,不设置则鼠标滑过点标无文字提示
String title;
/// 添加文本标注
String label;
/// 点标记是否可点击(默认为true)
bool clickable;
/// 设置点标记是否可拖拽移动(默认为false)
bool crossOnDrag;
/// Marker 点击事件
final click;
/// Marker点击显示窗体信息
InfoWindow clickInfoWindow;
Marker(this.position, { this.title, this.label, this.icon, this.clickable = true,
this.crossOnDrag = false, this.clickInfoWindow, this.click});
@override
String toString() {
return 'Marker{position: $position, icon: $icon, title: $title, label: $label, clickable: $clickable, crossOnDrag: $crossOnDrag, '
'clickInfoWindow: $clickInfoWindow }';
}
Map<String, dynamic> toJson() {
Map<String, dynamic> _m = new Map<String, dynamic>();
_m["position"] = this.position.toJson();
_m["icon"] = this.icon;
_m["title"] = this.title;
_m["label"] = this.label;
_m["clickable"] = this.clickable;
_m["crossOnDrag"] = this.crossOnDrag;
_m["clickInfoWindow"] = (null != this.clickInfoWindow) ? this.clickInfoWindow.toJson():null;
return _m;
}
factory Marker.fromJson(Map<String, dynamic> json) {
return Marker(
json['position'],
title: json['title'],
label: json['label'],
icon: json['icon'],
clickable: json['clickable'],
crossOnDrag: json['crossOnDrag']);
}
}
import 'Marker.dart';
import 'Lnglat.dart';
class Line {
List<Lnglat> path;
// 线路颜色
String color;
// 线路宽
double width;
// 是否允许播放(默认false)
bool isAllowPlay;
// 线路端点开启
bool endPointEnable;
// 起点(预留参数;当endPointEnable 为true时,该参数生效)
Marker start;
// 终点(预留参数;当endPointEnable 为true时,该参数生效)
Marker end;
// 是否自动播放(默认false;当isAllowPlay 为true时,该参数生效)
bool isMoveAlong;
Line(this.path, {this.color, this.width, this.isAllowPlay, this.start, this.end, this.isMoveAlong = false, this.endPointEnable = false,});
@override
String toString() {
return 'Line{path: $path, isMoveAlong: $isMoveAlong}';
}
Map<String, dynamic> toJson() {
Map<String, dynamic> _m = new Map<String, dynamic>();
List<dynamic> _path = [];
if(null != this.path && this.path.length > 0){
for(int i = 0;i < this.path.length;i++){
_path.add(this.path[i].toJson());
}
}
_m["path"] = _path;
_m["color"] = this.color;
_m["width"] = this.width;
_m["isAllowPlay"] = this.isAllowPlay;
_m["start"] = this.start.toJson();
_m["end"] = this.end.toJson();
_m["isMoveAlong"] = this.isMoveAlong;
_m["endPointEnable"] = this.endPointEnable;
return _m;
}
factory Line.fromJson(Map<String, dynamic> json) {
return Line(
json['path'],
color: json['color'],
width: json['width'],
isAllowPlay: json['isAllowPlay'],
start: json['start'],
end: json['end'],
isMoveAlong: json['isMoveAlong'],
endPointEnable: json["endPointEnable"]);
}
}
enum MapLocale {
/// 地图支持语言种类详见:https://developers.google.cn/maps/faq#languagesupport
/// English (EN) United States
en_us,
/// Chinese (ZH) Simplified
zh_cn,
}
import 'dart:ui';
import 'gmap/Line.dart';
import 'gmap/Lnglat.dart';
import 'gmap/MapLocale.dart';
import 'gmap/Marker.dart';
class MapOption {
// 地图语言(默认英文)
MapLocale locale;
// 地图默认中心点
Lnglat center;
// 显示控件(默认 开启)
bool controlEnable;
// 允许拖拽地图(默认 开启)
bool dragEnable;
// 双击缩放地图(默认 开启)
bool doubleClickZoom;
// 地图点
List<Marker> markers;
// 地图线
List<Line> lines;
// 地图缩放级别
num zoom;
// 地图加载完成回调
final complete;
// Marker点击回调
final markerClick;
MapOption({this.locale = MapLocale.en_us, this.center, this.markers, this.lines, this.zoom, this.complete, this.markerClick, this.controlEnable = true, this.dragEnable = true, this.doubleClickZoom = true,});
@override
String toString() {
return 'GMapOption{center: $center, markers: $markers, lines: $lines, zoom: $zoom, controlEnable: $controlEnable, dragEnable:$dragEnable, doubleClickZoom:$doubleClickZoom }';
}
Map<String, dynamic> toJson () {
Map<String, dynamic> _m = new Map<String, dynamic>();
_m["center"] = this.center.toJson();
List<dynamic> _markers = [];
if(null!= this.markers && this.markers.length > 0){
for(int i = 0;i < this.markers.length;i++){
_markers.add(this.markers[i].toJson());
}
}
_m["markers"] = _markers;
List<dynamic> _lines = [];
if(null != this.lines && this.lines.length > 0) {
for(int i = 0;i < this.lines.length;i++){
_lines.add(this.lines[i].toJson());
}
}
_m["lines"] = _lines;
_m["zoom"] = this.zoom;
_m["dragEnable"] = this.dragEnable;
_m["controlEnable"] = this.controlEnable;
_m['doubleClickZoom'] = this.doubleClickZoom;
return _m;
}
factory MapOption.fromJson(Map<String, dynamic> json) {
return MapOption(
center: json['center'],
markers: json['markers'],
lines: json['lines'],
zoom: json['zoom'],
dragEnable: json['dragEnable'],
controlEnable: json['controlEnable'],
doubleClickZoom: json['doubleClickZoom']
);
}
}
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'Config.dart';
import 'MapOption.dart';
import 'gmap/Line.dart';
import 'gmap/MapLocale.dart';
import 'gmap/Marker.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MapView extends StatefulWidget {
MapOption mapOption;
String cityName;
AppBar appBar;
WebViewController viewController;
MapView({this.mapOption, this.cityName, this.appBar});
@override
_MapViewState createState() => _MapViewState();
Object exceJavascript(String excute){
if(null != viewController){
viewController.evaluateJavascript('callbackDemo("Demo回调Demo回调Demo回调Demo回调Demo回调");').then((result) {
//print('您可以在此处处理JS结果1');
return '您可以在此处处理JS结果1';
});
}
}
}
class _MapViewState extends State<MapView> {
bool _loading = true;
WebView webViewx;
WebViewController _vc;
dynamic overlayEncode(dynamic item) {
if(item is Marker) {
return item.toJson();
}else if(item is Line) {
return item.toJson();
}
return item;
}
///js与flutter交互
JavascriptChannel _alertJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toast',//invoke要和网页协商一致
onMessageReceived: (JavascriptMessage message) {
if(null == widget.viewController){
setState(() {
widget.viewController = _vc;
});
}
var msg = json.decode(message.message);
//print(msg['isFirstComplete']);
if(null != msg['isFirstComplete'] && true == msg['isFirstComplete']){ // 地图首次加载
//print('地图首次加载完成');
if(null != widget.mapOption.markers && widget.mapOption.markers.length > 0){/// 回调页面绘制点的方法
var _markers = json.encode(widget.mapOption.markers, toEncodable: overlayEncode);
widget.viewController.evaluateJavascript('setMarkers(\''+_markers+'\');').then((result) {
//print('您可以在此处处理绘制Marker的业务逻辑');
if(null != widget.mapOption.zoom && widget.mapOption.zoom > 0 && widget.mapOption.zoom < 22){
widget.viewController.evaluateJavascript('setZoom(\''+widget.mapOption.zoom.toString()+'\');').then((result) {
//print('您可以在此处处理地图缩放的业务逻辑');
});
}
});
}else if(null != widget.mapOption.zoom && widget.mapOption.zoom > 0 && widget.mapOption.zoom < 22){
widget.viewController.evaluateJavascript('setZoom(\''+widget.mapOption.zoom.toString()+'\');').then((result) {
//print('您可以在此处处理地图缩放的业务逻辑');
});
}
widget.mapOption.complete(widget.viewController, msg);
}else{
// 业务回调
if(null != msg['MARKER_CLICK'] && true == msg['MARKER_CLICK']){// Marker Click 回调
//print('Marker Click 回调');
widget.mapOption.markerClick(widget.viewController, msg);
}
}
});
}
String getLanguage(MapLocale _mapLocale) {
String _language = "";
switch (_mapLocale) {
case MapLocale.en_us:
_language = "en";
break;
case MapLocale.zh_cn:
_language = "zh";
break;
default:
_language = "en";
}
return _language;
}
@override
void initState() {
String _serverUrl = "http://map.bkybk.com/gmap_flutter.html?";
var _UrlParam = "";
if(null != widget.mapOption.center){
_UrlParam += "center=" + widget.mapOption.center.lng.toString() + "," + widget.mapOption.center.lat.toString();
}
if(null != widget.mapOption.controlEnable && false == widget.mapOption.controlEnable){
if(0 < _UrlParam.length){
_UrlParam += "&";
}
_UrlParam += "controlEnable=0";
}
if(null != widget.mapOption.dragEnable && false == widget.mapOption.dragEnable){
if(0 < _UrlParam.length){
_UrlParam += "&";
}
_UrlParam += "dragEnable=0";
}
if(null != widget.mapOption.doubleClickZoom && false == widget.mapOption.doubleClickZoom){
if(0 < _UrlParam.length){
_UrlParam += "&";
}
_UrlParam += "doubleClickZoom=0";
}
if(Platform.isIOS){
if(0 < _UrlParam.length){
_UrlParam += "&";
}
_UrlParam += "platform=IOS";
} else {
if(0 < _UrlParam.length) {
_UrlParam += "&";
}
_UrlParam += "platform=Android";
}
/// 地图语言
if(0 < _UrlParam.length){
_UrlParam += "&";
}
String _language = getLanguage(widget.mapOption.locale);
_UrlParam += "language="+_language;
_serverUrl = _serverUrl + _UrlParam;
super.initState();
//使用插件 FaiWebViewWidget
webViewx = WebView(
initialUrl: _serverUrl,///初始化url
javascriptMode: JavascriptMode.unrestricted,///JS执行模式),
onWebViewCreated: (WebViewController webViewController) {///在WebView创建完成后调用,只会被调用一次
//setState(() {
_vc = webViewController;
widget.viewController = webViewController;
//});
//widget._viewController = webViewController;
},
onPageFinished: (String url) {///页面加载完成回调
setState(() {
_loading = false;
});
// TODO 去掉遮罩(遮罩层未实现)
},
javascriptChannels: <JavascriptChannel>[///JS和Flutter通信的Channel;
_alertJavascriptChannel(context),
].toSet(),
navigationDelegate: (NavigationRequest request) {//路由委托(可以通过在此处拦截url实现JS调用Flutter部分);
///通过拦截url来实现js与flutter交互
if (request.url.startsWith('js://webview')) {
// Fluttertoast.showToast(msg:'JS调用了Flutter By navigationDelegate');
print('blocking navigation to $request}');
return NavigationDecision.prevent;///阻止路由替换,不能跳转,因为这是js交互给我们发送的消息
}
return NavigationDecision.navigate;///允许路由替换
},
);
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.appBar!=null?widget.appBar:null,
body: buildRefreshHexWidget(),
);
}
Widget buildRefreshHexWidget() {
return RefreshIndicator(
//下拉刷新触发方法
onRefresh: () async{
print('refresh');
},
//设置webViewWidget
child:webViewx,
);
}
}
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'common/Config.dart';
import 'common/MapOption.dart';
import 'common/MapView.dart';
import 'common/gmap/Lnglat.dart';
import 'common/gmap/MapLocale.dart';
void main(List<String> args) {
App();
}
class App extends StatefulWidget {
@override
AppState createState() => AppState();
}
class AppState extends State<App> {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Google Maps examples')),
backgroundColor: Colors.grey.shade200,
body: MapView(
appBar: null,
mapOption: MapOption(
locale: MapLocale.zh_cn,
controlEnable: false,
dragEnable: true,
doubleClickZoom: true,
center: Lnglat(116.396663, 39.912321),
complete: (_viewController, value) {
// 地图加载完成后回调
},
markerClick: (_viewController, res) {
// 地图上marker点击回调
}),
cityName: "北京",
),
),
);
}
}
以上核心代码是我进行过整理和一定的简单封装,主要是方便使用。开始运行。
搞定!!!
嵌套的页面地址是http://map.bkybk.com/gmap_flutter.html,关于这个html的源码我这里就贴了,感兴趣的可以自己去把页面的代码复制出来喔~
http://map.bkybk.com/gmap_flutter.html 这个地址现在显示不了
由于目前Google地图已经被国内和谐了。所以这个地图也就加载不出来了。不过,你可以尝试使用MapBox或者百度、高德国际版地图去替换Google地图。
我个人感觉MapBox地图和Google地图两者地图数据相似,可以平替,API也基本相似,且是全球地图数据。
至于这个页面的代码,你可以直接把这个抓下去研究一下和flutter之间的通信交互。
楼主,最近Google地图api也用不了了啊。你有啥解决方法么?
Google地图我记得在疫情期间好像就停止国内的服务了。目前我想到且替换成本较低的就是MapBox这个产品了,这个Google的API基本一样,且也支持国际化。