Flutter插件开发之Android高德地图
从零开发一个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使用 Java,iOS使用 Swift。本次主要以Android插件开发为主。至于iOS的插件开发,笔者会在后期更新
等待Android Studio生成项目成功后,出现项目的 结构如图 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所示
整个界面没有一点报红的地方。现在可以正式编写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 为笔者机型的效果
授予相关的权限之后再试试效果;这样就成功显示效果,如图12所示
注意事项
有几点需要注意
- 修改了原生代码之后,建议重新编译运行看效果(shift + f9)
- 虽然在AndroidManifest.xml申请了权限,但是还是有可能会出现未授予相关权限导致的报错
- 混合编程最大的难点是定位错误的位置,不要一昧地觉得是某一端的报错,建议先排除是 Dart 端的错误再 来定位 Native端
- 注意几个 字符串的 对应,比如 Dart 与 Native的 MethodChannel的对应 ,注册view与 Dart View 的对应问题
相关代码在我的github上 easy_flutter_amap 我会根据star热度跟个人空余时间来持续地维护这个项目,后期会上 Swift iOS,如果有大佬愿意加入iOS插件的编写,那就更好了
总结
笔者在此之前,使用了 amap_map_fluttify 这个插件,截止本文发布之前,我感觉这是最好用的 flutter 高德地图插件(甚至优于官方的插件),可惜作者维护频率很低,有很多Bug都未修复。笔者如果可能的话,可能也会继续维持一个好的库维护开发。
最近笔者在公司使用Flutter技术独自开发一款企业级的物联网应用,如果对笔者感兴趣,欢迎关注笔者。读者有好的建议,也欢迎在下面留言。码字不易,请给
更多相关文章
- Android连接mysql demo_Android实现登陆功能,Android与服务器数据
- 【quickhybrid】Android端的项目实现
- Android的开源隐忧:品牌稀释 代码分裂
- Android(安卓)JNI入门
- Android的开源隐忧:品牌稀释 代码分裂
- 扬州旅游app(一)
- android插件开发机制研究
- 关于Android高德地图4.12无法显示地图只显示Logo问题,非只添加 j
- Android从零撸美团(四) - 美团首页布局解析及实现 - Banner+自定