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使用 JavaiOS使用 Swift。本次主要以Android插件开发为主。至于iOS的插件开发,笔者会在后期更新

Flutter插件开发之Android高德地图_第1张图片

图 1


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

Flutter插件开发之Android高德地图_第2张图片

图 2


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

第二步:编写Android原生插件

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

Flutter插件开发之Android高德地图_第3张图片

图 3


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

Flutter插件开发之Android高德地图_第4张图片

图4


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

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

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

Flutter插件开发之Android高德地图_第5张图片

图5

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

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

Flutter插件开发之Android高德地图_第6张图片

图6

Flutter插件开发之Android高德地图_第7张图片
图7

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

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

Flutter插件开发之Android高德地图_第8张图片

图7-1

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

Flutter插件开发之Android高德地图_第9张图片

图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所示

Flutter插件开发之Android高德地图_第10张图片

图8

Flutter插件开发之Android高德地图_第11张图片

图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所示

Flutter插件开发之Android高德地图_第12张图片

图10


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

Flutter插件开发之Android高德地图_第13张图片

图11


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

Flutter插件开发之Android高德地图_第14张图片

图12

Flutter插件开发之Android高德地图_第15张图片

控制台打印效果

注意事项

有几点需要注意

  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技术独自开发一款企业级的物联网应用,如果对笔者感兴趣,欢迎关注笔者。读者有好的建议,也欢迎在下面留言。码字不易,请给

你可能感兴趣的:(Flutter,移动开发,android,flutter)