从零开发一个Android高德地图插件

前言

看过很多博客文章,发现很少提到关于实战中如何使用Android与Dart的混编交互,Flutter在实际项目中仍然需要大量运用到原生的一些功能,比如相机,地图,访问设备本地相册等功能的需求,尽管有大量的第三方插件提供了这些功能,但是实际项目开发中,很可能是不太符合需求的,所以必须要掌握如何在Flutter中使用Android或者iOS的原生View,及Dart与原生的数据交互。本次笔者在项目中接入高德地图,从而向读者展示如何在Flutter中嵌入AndroidView,并且能让Dart与Java交互。

效果预览



进入高德地图显示个人的位置,并以个人的位置为居中点,点击按钮通过 Dart 层向 Native层发送参数,Native根据 Dart 层发送的参数,动态生成 marker显示,回调参数给Dart 层。

实现需求

实现需求 是否实现
3D地图
显示定位
自定义marker显示
Dart 向 Native发送数据
Native 向Dart发送数据

开发环境

Flutter 1.22.6.stable
Android SDK minSdkVersion:21 compileSdkVersion:29
Gradle 6.5
测试机型 华为荣耀 Play4T 系统Android 10
IDE Android Sutdio 4.2

实现过程

第一步:新建一个Flutter Plugin工程,然后配置一些项目信息,如图1所示。
值得注意的是插件的语言 Android使用 JavaiOS使用 Swift。本次主要以Android插件开发为主。至于iOS的插件开发,笔者会在后期更新

图 1


等待Android Studio生成项目成功后,出现项目的 结构如图 2所示

图 2


其实看上去跟普通的Flutter工程没什么区别,多了个example目录用来测试插件功能,那么接下来开始编写原生代码插件。

第二步:编写Android原生插件

接下来是重点: 如何进入Android原生代码的编辑呢? 很多博客文章并没有完整地展示如何进入Android原生代码的编辑,我刚开始以为是用 Android Studio直接open到项目根目录下的 android文件夹进行Android原生代码的编写,但是结果完全行不通,各种报错,比如找不到 xxx类。然后又搜索了找不到 Flutter 相关类的问题,又是各种导入 Flutter 依赖,又是增加什么环境变量。但是直到我看一个 Flutter 插件的教学视频,才恍然大悟,原来是我打开的方式有问题。话不多说,看 图 3

图 3


相信各位应该能看得很清楚了,鼠标右击项目根目录,然后点击 Flutter 来引出 Open Android Module in Android Studio,等待一会的gradle build之后 这样就进入到了Android原生插件的编辑。结果如图4所示

图4


整个界面没有一点报红的地方。现在可以正式编写Android 原生代码,值得注意的一点是:还记得我们一开始提到的 用于测试 插件的 example目录吗,没错那个example目录对应的其实是现在的app目录,而我们要在 easy_flutter_amap这个目录编写原生代码
要接入高德地图需要到移步到官方的 高德地图开放平台 申请key,官网有详细的申请流程与文档参考。

本次使用时高德最新的 3D地图,,先在 如图 5 插件的build.gradle 位置添加高德地图的依赖

dependencies {         implementation('com.amap.api:3dmap:7.8.0')}

图5

如图5,注意下划红线的部分代码,设置SDK的最低兼容版本为19, 然后 点击 sync,开始下载高德地图的依赖。

在我这个项目目录中,默认是没有res 目录的,所以要建立一个 res资源目录,如图6 图7

图6
图7

这样就成功地建立了 res 资源目录,接下来再建立一个 drawable目录用来存放一些图标(其实在Flutter项目中,图片的管理是比较复杂的,因为图片实际上既可以放到flutter项目的assets目录,也可以放到原生的目录当中,如果管理不当,很容易出现同样的图片在多端存在的情况,增大了应用app的体积,在笔者面试过的一家公司当中,就出现了这样很严重的问题)

在res目录中的drawable文件夹中添加 mime.png 图片作为 显示自己位置的图标,效果如图 7-1所示

图7-1

再添加 location_marker.png 图片作为 mark标记的 图片,如图 7-2 所示

图7-2

新建 AmapView.java文件,通过 params从 AmapViewFactory传来的参数,来初始化高德地图的一些配置信息,并实现与 Dart 交互的接口,从而让达到跨平台的目的。这些代码的目的是:居中显示个人的位置,并且能动态添加 marker标记点,并且添加marker标记点的 参数(标题,经纬度)通过Dart端传递过来,使用原生Native渲染,而这个方法的管道名称为 easy_flutter_amap ,所以 Dart端的 MethodChannel也得连接名称为 easy_flutter_amap,添加成功之后 Native端会返回 一个字符串 suc 给 Dart层。

package cn.jjvu.xiao.easy_flutter_amap.view;import android.content.Context;import android.graphics.BitmapFactory;import android.graphics.Color;import android.os.Bundle;import android.util.Log;import android.view.View;import androidx.annotation.NonNull;import com.amap.api.maps.AMap;import com.amap.api.maps.CameraUpdateFactory;import com.amap.api.maps.LocationSource;import com.amap.api.maps.MapView;import com.amap.api.maps.model.BitmapDescriptorFactory;import com.amap.api.maps.model.LatLng;import com.amap.api.maps.model.MarkerOptions;import com.amap.api.maps.model.MyLocationStyle;import java.util.Map;import cn.jjvu.xiao.easy_flutter_amap.R;import io.flutter.plugin.common.BinaryMessenger;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugin.platform.PlatformView;public class AmapView implements PlatformView, MethodChannel.MethodCallHandler {         MapView mapView;    AMap aMap;    LocationSource.OnLocationChangedListener mListener;    private MethodChannel methodChannel;    private Context context;    private static final String TAG = "AmapView";    private Map<String, Object> initParams;    public AmapView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {             Log.d(TAG, params.toString());        methodChannel = new MethodChannel(messenger, "easy_flutter_amap");        methodChannel.setMethodCallHandler(this);        initParams = params;        createMap(context);        initMapOptions();        mapView.onResume();        this.context = context;    }    @Override    public View getView() {             return mapView;    }    @Override    public void dispose() {             mapView.onDestroy();    }    private void createMap(Context context) {             mapView = new MapView(context);        mapView.onCreate(new Bundle());        aMap = mapView.getMap();    }    private void initMapOptions() {             Log.d(TAG, initParams.toString());        aMap.moveCamera(CameraUpdateFactory.zoomTo(Float.parseFloat(initParams.get("zoomLevel").toString())));        aMap.getUiSettings().setMyLocationButtonEnabled(true);        MyLocationStyle myLocationStyle = new MyLocationStyle();        myLocationStyle.interval(Long.parseLong(initParams.get("interval").toString()));        myLocationStyle.strokeWidth(1f);        myLocationStyle.strokeColor(Color.parseColor("#8052A3FF"));        myLocationStyle.radiusFillColor(Color.parseColor("#3052A3FF"));        myLocationStyle.showMyLocation(true);        myLocationStyle.myLocationIcon(BitmapDescriptorFactory.fromResource(R.drawable.mime));        myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);        aMap.setMyLocationStyle(myLocationStyle);        aMap.setMyLocationEnabled(true);    }    @Override    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {             if (call.method.equals("addMarkers")) {                 if (call.arguments != null) {                     Map<String, Object> datas = (Map<String, Object>) call.arguments;                showMarker(datas);                result.success("suc");            }        }    }    private void showMarker(Map<String, Object> data) {             MarkerOptions markerOption = new MarkerOptions();        markerOption.position(new LatLng((double) data.get("latitude"), (double) data.get("longitude")));        markerOption.title((String) data.get("title"));        markerOption.draggable(false);        markerOption.icon(BitmapDescriptorFactory.fromBitmap(BitmapFactory            .decodeResource(this.context.getResources(), R.drawable.location_marker)));        markerOption.setFlat(true);        aMap.addMarker(markerOption);    }}

新建 AmapViewFactory.java 文件,代码如下。将会从这里中转Flutter端获取到的参数,在Native View实例化的时候,注入Flutter端传递来的参数。

package cn.jjvu.xiao.easy_flutter_amap.view;import android.app.Activity;import android.content.Context;import java.util.Map;import io.flutter.plugin.common.BinaryMessenger;import io.flutter.plugin.common.StandardMessageCodec;import io.flutter.plugin.platform.PlatformView;import io.flutter.plugin.platform.PlatformViewFactory;public class AmapViewFactory extends PlatformViewFactory {         private BinaryMessenger messenger;    private Activity activity;    public AmapViewFactory(BinaryMessenger messenge, Activity activity) {             super(StandardMessageCodec.INSTANCE);        this.activity = activity;        this.messenger = messenge;    }    @Override    public PlatformView create(Context context, int id, Object args) {             Map<String, Object> params = (Map<String, Object>) args;        return new AmapView(context, messenger, id, params);    }}

在 EasyFlutterAmapPlugin .java文件中注册 我们的地图插件。

package cn.jjvu.xiao.easy_flutter_amap;import android.app.Activity;import androidx.annotation.NonNull;import cn.jjvu.xiao.easy_flutter_amap.view.AmapViewFactory;import io.flutter.embedding.engine.plugins.FlutterPlugin;import io.flutter.embedding.engine.plugins.activity.ActivityAware;import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;import io.flutter.plugin.common.BinaryMessenger;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugin.common.MethodChannel.MethodCallHandler;import io.flutter.plugin.common.MethodChannel.Result;import io.flutter.plugin.platform.PlatformViewRegistry;public class EasyFlutterAmapPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {       private MethodChannel channel;    private BinaryMessenger messenger;    private PlatformViewRegistry platformViewRegistry;  @Override  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {         messenger = flutterPluginBinding.getBinaryMessenger();;    platformViewRegistry = flutterPluginBinding.getPlatformViewRegistry();  }  @Override  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {         if (call.method.equals("getPlatformVersion")) {           result.success("Android " + android.os.Build.VERSION.RELEASE);    } else {           result.notImplemented();    }  }  @Override  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {         channel.setMethodCallHandler(null);  }  @Override  public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {         Activity activity = binding.getActivity();    platformViewRegistry.registerViewFactory("cn.jjvu.xiao.easy_flutter_amap/mapview", new AmapViewFactory(messenger, activity));  }  @Override  public void onDetachedFromActivityForConfigChanges() {       }  @Override  public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {       }  @Override  public void onDetachedFromActivity() {       }}

在插件的 AndroidManifest.xml目录中 增加一些权限 还有高德地图的必要配置,具体以高德地图的官网说明为准,如图8,9所示

图8

图9

有些权限问题,在本文不作详细描述,如果有需要请看 Flutter Android权限问题,在几个xml文件中配置好 高德的key,这样就完成了初步的 高德地图原生代码的编写,再开始编写 Dart跨平台代码

在 eas_flutter_amap.dart文件中编写 AndroidView类,用AmapConfig类来配置一些高德地图的参数,本次示例简单一点配置初始缩放参数跟,刷新位置时间间隔,让Flutter端在Android View实例化的时候,往 Native端发送数据

import 'package:flutter/material.dart';import 'package:flutter/services.dart';class AmapView extends StatelessWidget {  AmapConfig config;  MethodChannel _channel = MethodChannel('easy_flutter_amap');  AmapView({this.config}) {    if (null == this.config)      this.config = AmapConfig();  }  @override  Widget build(BuildContext context) {    return Container(      child: AndroidView(        viewType:"cn.jjvu.xiao.easy_flutter_amap/mapview",        creationParamsCodec: StandardMessageCodec(),        creationParams: config.toMap(),      ),    );  }  Future addMarker(MarkerOption options) async {    return _channel.invokeMethod("addMarkers", options.toMap());  }}class AmapConfig {  int interval;  double zoomLevel;  AmapConfig({this.interval: 1000, this.zoomLevel: 28.0});  Map toMap() {    Map map = Map();    map['interval'] = interval;    map['zoomLevel'] = zoomLevel;    return map;  }}class MarkerOption {  double latitude;  double longitude;  String title;  MarkerOption({this.latitude, this.longitude, this.title});  Map toMap() {    Map map = Map();    map['latitude'] = latitude;    map['longitude'] = longitude;    map['title'] = title;    return map;  }}

最后我们测试一下插件

在 example/lib/main.dart文件中编辑以下代码,用来测试插件,并打印 回调的 参数

import 'package:flutter/material.dart';import 'package:easy_flutter_amap/easy_flutter_amap.dart';void main() {  runApp(MyApp());}class MyApp extends StatefulWidget {  @override  _MyAppState createState() => _MyAppState();}class _MyAppState extends State {  AmapView amapView;    @override  void initState() {    super.initState();  }  @override  Widget build(BuildContext context) {    amapView =  AmapView(      config: AmapConfig(zoomLevel: 3),    );    return MaterialApp(      home: Scaffold(        appBar: AppBar(          title: const Text('Plugin example app'),        ),        body: Center(          child: amapView        ),        floatingActionButton: FloatingActionButton(          child: Text('添加marker'),          onPressed: () async {            String msg = await amapView.addMarker(MarkerOption(latitude: 34.341568, longitude: 108.940174, title: "标记"));            debugPrint(msg);          },        ),      ),    );  }}

运行 main.dart文件,结果出现问题:没有显示我的定位,以及地图中心点在北京,如图10所示

图10


感觉代码应该是没问题的,问题会出现在哪呢?对了,可能是权限问题,因为我们这个测试项目暂时还未集成 permission、permission_handler这样的flutter插件,所以还需要手动设置以下权限,如图 11所示,
每个机型的设置可能都不太一样,图11 为笔者机型的效果

图11


授予相关的权限之后再试试效果;这样就成功显示效果,如图12所示

图12

控制台打印效果

注意事项

有几点需要注意

  1. 修改了原生代码之后,建议重新编译运行看效果(shift + f9)
  2. 虽然在AndroidManifest.xml申请了权限,但是还是有可能会出现未授予相关权限导致的报错
  3. 混合编程最大的难点是定位错误的位置,不要一昧地觉得是某一端的报错,建议先排除是 Dart 端的错误再 来定位 Native端
  4. 注意几个 字符串的 对应,比如 Dart 与 Native的 MethodChannel的对应 ,注册view与 Dart View 的对应问题

相关代码在我的github上 easy_flutter_amap 我会根据star热度跟个人空余时间来持续地维护这个项目,后期会上 Swift iOS,如果有大佬愿意加入iOS插件的编写,那就更好了

总结

笔者在此之前,使用了 amap_map_fluttify 这个插件,截止本文发布之前,我感觉这是最好用的 flutter 高德地图插件(甚至优于官方的插件),可惜作者维护频率很低,有很多Bug都未修复。笔者如果可能的话,可能也会继续维持一个好的库维护开发。

最近笔者在公司使用Flutter技术独自开发一款企业级的物联网应用,如果对笔者感兴趣,欢迎关注笔者。读者有好的建议,也欢迎在下面留言。码字不易,请给

更多相关文章

  1. Android连接mysql demo_Android实现登陆功能,Android与服务器数据
  2. 【quickhybrid】Android端的项目实现
  3. Android的开源隐忧:品牌稀释 代码分裂
  4. Android(安卓)JNI入门
  5. Android的开源隐忧:品牌稀释 代码分裂
  6. 扬州旅游app(一)
  7. android插件开发机制研究
  8. 关于Android高德地图4.12无法显示地图只显示Logo问题,非只添加 j
  9. Android从零撸美团(四) - 美团首页布局解析及实现 - Banner+自定

随机推荐

  1. Android 正则表达式验证手机和邮箱格式是
  2. Android 2.1 android.R.drawable Icon Re
  3. Android设置去掉 外部USB存储和默认存储
  4. android studio 错误: 找不到符号 符号:
  5. 【 Android '四大组件' 】篇 -- Activity
  6. 2.5.6 使用progressDialog创建进度对话框
  7. 关于android xml文件中 android:id="@+id
  8. 【Android(安卓)NDK】(一)Hello World!
  9. android自定义title
  10. Android——使用GridView制作二维布局界