由于项目需要开发一款国际化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的源码我这里就贴了,感兴趣的可以自己去把页面的代码复制出来喔~

标签: Flutter, Flutter实现Google地图, 国内使用Google地图, 移动端Google地图

已有 4 条评论

  1. ljx

    http://map.bkybk.com/gmap_flutter.html 这个地址现在显示不了

    1. 由于目前Google地图已经被国内和谐了。所以这个地图也就加载不出来了。不过,你可以尝试使用MapBox或者百度、高德国际版地图去替换Google地图。
      我个人感觉MapBox地图和Google地图两者地图数据相似,可以平替,API也基本相似,且是全球地图数据。
      至于这个页面的代码,你可以直接把这个抓下去研究一下和flutter之间的通信交互。

  2. bb

    楼主,最近Google地图api也用不了了啊。你有啥解决方法么?

    1. Google地图我记得在疫情期间好像就停止国内的服务了。目前我想到且替换成本较低的就是MapBox这个产品了,这个Google的API基本一样,且也支持国际化。

添加新评论