navi

前言

在国内地图使用最多的应该是高德和百度,对于Flutter来说高德地图在两年前就有了比较成熟方便使用的三方库。反观百度地图,去年上半年的时候开始推出Flutter版本,但是是手动集成的,今年再来看已经有了定位、地图、鹰眼的Flutter版本,而且是通过pub管理的。今年有个项目只需要百度地图的导航功能,但是官方恰恰就没有提供,搜了一下网上好像也没有相关的库。所以就动手自己封了一个简单的百度导航插件,由于自身以前是iOS开发,所以暂时只提供了iOS端的插件,Android方面待后续完善,也希望有此需求的Android开发能够帮忙完善。

百度地图导航iOS端开发

Swift尝试

由于平时开发中主要是使用Swift,所以一开始创建Flutter插件的时候就选择的Swift进行开发。但是在这个过程中遇到一些问题,主要是库引入相关。

  1. 使用pod方式引入BaiduNaviKit,即在podspec中添加导航pod包
1
s.dependency 'BaiduNaviKit'

问题:百度导航SDK在swift文件中无法通过import BaiduNaviKit的形式引入。在普通的Swift和OC混编工程中,可以通过桥接文件xxx-Bridging-Header.h引入,但是在pod开发中是不支持桥接文件的。

解决:尝试通过 https://lingjye.com/2018/06/18/Component-problem/ 中提到的通过设置private_header_files的方式来充当桥接文件

1
2
# podspec配置
s.private_header_files = 'Classes/BridgeHeader.h'
1
2
3
4
5
6
7
8
// 自定义的头文件,尝试充当桥接文件,引入导航相关头文件
#ifndef BridgeHeader_h
#define BridgeHeader_h

#import "BNaviService.h"
#import <BaiduMapAPI_Base/BMKBaseComponent.h>

#endif /* BridgeHeader_h */

但是pod install之后在umbrella.h中并没有引入自定义的BridgeHeader.h文件,自然也就没法使用。不知道哪里没有设置正确,如果有pod配置方面比较熟悉的朋友,欢迎提出正确的解决方法。

  1. 使用手动引入Framework的方式,如以下配置:
1
2
3
4
5
6
7
# BaiduNaviSDK/NaviSDK/inc/*.h 引入导航头文件
s.source_files = 'Classes/**/*', 'BaiduNaviSDK/NaviSDK/inc/*.h'
# 引入导航相关的.a和.framework包
s.vendored_libraries = 'BaiduNaviSDK/**/*.a'
s.vendored_frameworks = 'BaiduNaviSDK/**/*.framework'
# 不使用pod依赖,注释掉
# s.dependency 'BaiduNaviKit'

问题:报错Include of non-modular header inside framework module 'baidu_map.BridgeHeader,因为导航依赖了百度地图基础库,即在NaviSDK中的头文件中引用了百度地图的头文件,如在BNaviService.h中引入了#import <BaiduMapAPI_Map/BMKMapView.h>就会报错。

解决:暂时不知道怎么解决,我猜只有百度官方更新更好的适配Swift才行。

期望

  1. 官方的导航SDK支持import module的形式,可以让Swift直接调用
  2. 官方更改目前.h与.a .framework混合的方式,导航SDK统一采用和地图、定位一样的纯framework方式,满足手动配置的需求

转用OC

因为上述情况,Swift并不能很方便的接入百度导航。想到OC可以直接引入其头文件,省去了桥接、库引用带来的各种问题,固重新建了以OC为开发语言的Flutter插件。

接入方式选择,pod和Framework手动

因为OC可以直接引用头文件,所以直接选择了pod引入的方式,目前指定了最新的6.2.0版本

1
s.dependency 'BaiduNaviKit', '6.2.0'

静态库配置问题

加入pod依赖install后,.podspec不做任何额外配置的情况下,还是遇到了一些报错(警告):

  1. The ‘Pods-XXX‘ target has transitive dependencies that include statically linked binaries

这个错误就是百度导航SDK里面用到的.a .framework是静态库的原因

  1. Undefined symbols for architecture arm64

以上两个问题可通过配置static_framework = true解决,即

1
s.static_framework = true

百度导航语音问题

解决库的引入依赖问题后,参考百度官方的文档编写导航相关的代码,使用正确的地图应用AppKey授权导航,即可以正常的在应用内使用导航功能,授权代码调用authorizeNaviAppKey

但是会发现导航没有声音,及时我在百度AI开放平台注册了和地图BundleID一样的应用,同时也参考官方文档申请了免费额度的语音合成服务。在导航初始化时调用TTS授权代码,其回调的结果永远是NO不成功。

1
2
3
4
[[BNaviService getInstance] authorizeTTSAppId:ttsAppId apiKey:ttsApiKey secretKey:ttsSecretKey completion:^(BOOL ttsResult) {
// ttsResult结果为NO,使用的AI开放平台正确的Key
NSLog(@"authorizeTTS ret = %d",ttsResult);
}];

百度官方的文档比较老了,语音播报模块TTS授权管理还是http://yuyin.baidu.com/app, 跳转后是到AI开放平台,其使用说明和文档的出入还是比较大。现在的AI开放平台功能更复杂,很多也需要付费。我申请了语音模块,没有付费,申请了免费额度,不知道是不是这个原因造成的没有声音。为了这个问题,专门提了工单,建议更新下文档,提供下解决方案。官方答复检查Product Name是否为英文,我检查了工程是没问题的。

解决:使用内置的TTS语音播报功能

下载了官方的导航demo,使用自己的BundleID和相关的Key包括TTS相关Key,运行起来是有导航声音的,但是断点查看TTS授权代码回调结果依旧是NO

对比了下两者的区别,官方demo是采用手动配置SDK的方式,对比发现其使用了带TTS相关的静态库和资源文件,所以基本可以猜测是使用的内置TTS语音播报。再去仔细看了下官方的文档,第一句话就说明了:使用SDK内置百度TTS语音播报功能需要导入libBNTTSComponentSDK.a静态库,并且需要对应用进行授权验证才能够使用,因此需要主动注册应用相关信息

注:使用内置的TTS播报功能,必须导入libBNTTSComponentSDK.a静态库和baiduTTSSDK.bundle资源文件。而且必须要注册TTS相关的Key,一个很奇怪的现象是:使用了Key调用TTS授权代码,回调仍然是NO不成功,但是会有导航声音,如果不传任何的Key,就会没有导航声音。而且我iOS申请的TTS Key拿给Android用也能生效,导航有声音(包名不同)。所以对TTS授权这些Key的实际使用和相关准确配置现在都还有一个问号。现在能保证的是传入申请的Key,使用内置语音奏效!

插件中配置声音

更新:直接使用pod配置TTS即可,在podspect中添加:

1
s.dependency 'BaiduNaviKit/TTS'

再次看了下百度导航官方的文档,发现TTS是支持pod依赖的,因为是注释掉了所以不起眼当时没有看到😂,还是自己不够仔细啊,走了那么多的弯路

插件中是使用pod的方式接入的导航SDK,里面是没有内置TTS相关静态库和资源文件的,于是就想到了直接将这两个声音依赖文件引入。将官方下载的libBNTTSComponentSDK.abaiduTTSSDK.bundle拷贝到了插件开发目录中(新建了Libs和Resources文件夹存放),并配置了.podspec文件如下:

1
2
3
4
5
# pod配置了TTS,手动添加的静态库和资源文件就不需要了,删除即可
# 配置静态库,此处只有libBNTTSComponentSDK.a
# s.vendored_libraries = 'Libs/*.a'
# 配置资源文件,此处只有baiduTTSSDK.bundle
# s.resource = ['Resources/*.bundle']

至此,导航有声音了,整个插件开发流程也就圆满了,再次附上关键的.podspec配置:

1
2
3
4
5
6
# 更新为pod依赖
s.dependency 'BaiduNaviKit', '6.2.0'
s.dependency 'BaiduNaviKit/TTS', '6.2.0'
s.static_framework = true
# s.vendored_libraries = 'Libs/*.a' #删除
# s.resource = ['Resources/*.bundle'] #删除

开启Background Modes -> Location updates

注意:根据官方文档,在接入自己的应用时不要忘了在Signing&Capabilities中添加Background Mode,选中Location updates,导航是需要后台播放更新的,使用过导航的都应该知道这个常识。如果不设置的话开始导航时会崩溃!

使用

添加依赖

因为Android端的功能尚未开发,固还未发布到pub,目前可使用git source依赖的方式,在pubspec.yaml中配置:

1
2
flutter_baidu_navi:
git: https://github.com/Smiacter/flutter_baidu_navi.git

或下载源码到本地,使用本地依赖插件的方式,在pubspec.yaml中配置插件目录路径

1
2
flutter_baidu_navi:
path: ../plugin/flutter_baidu_navi # 路劲根据自己的存放目录而定

初始化百度地图定位

1
2
3
4
5
6
7
8
9
/// 在适当位置调用初始化,由于使用内置TTS,返回mapSuccess即可
/// 参数依次为百度地图AppKey,TTS的AppId、ApiKey、SecretKey
/// TTS相关的Key为可选参数,如果不传默认为空,导航没声音
FlutterBaiduNavi.init(
"2GF3O2BTHxYHlMnoEuSFvyLo6c0xBn1h",
ttsAppId: "24147217",
ttsApiKey: "vewI9TV5VQRoOGPypStObVL3",
ttsSecretKey: "QaFknadW5sPlNEicIcrdFV4VeGG0KZvo",
);

初始化返回结果为BaiduNaviInitResult, 具体可查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 百度导航初始化结果
enum BaiduNaviInitResult {
/// 导航Appkey和TTS授权均成功
success,
/// 导航Appkey和TTS授权均失败【导航AppKey失败导航一定会失败,且会弹出未授权提示框,所以请确保地图应用的AppKey准确无误】
fail,
/// 导航AppKey成功,TTS失败
/// 注:实践证明,使用内置的TTS语音,有TTS相关Key,及时使用这些KeyTTS授权失败也会有导航声音,而且给包名完全不一样的Android使用也行!
/// 如果TTS相关的Key为空或者不是AI平台申请的Key,导航就很有可能没有声音
mapSuccessTtsFail,
/// 导航AppKey失败,TTS成功【导航AppKey失败导航一定会失败,且会弹出未授权提示框,所以请确保地图应用的AppKey准确无误】
mapFailTtsSuccess,
}

开始导航

1
2
3
4
5
6
7
/// 开始导航,参数依次为起点经度、起点纬度、终点经度、终点纬度
FlutterBaiduNavi.startNavi(
104.078063,
30.66664,
104.122785,
30.727933,
);

发起导航返回结果为BaiduNaviResult, 具体可查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// 百度导航结果
/// 注:选取了一些常见的错误,如需其他错误类型,需更改插件代码返回相应的值
enum BaiduNaviResult {
/// 成功开启导航
success,
/// 定位权限未开启或受限
locationUnauthorized,
/// 导航服务未初始化完成
naviServiceNotInited,
/// 网络不可用
networkError,
/// 起点与终点距离太近
tooNear,
/// 其他原因-导航失败
otherError,
}

TODO:

  • Android端功能开发
  • 最终完善,发布到pub

结尾

以上纯粹是个人的一些尝试与感悟,并没有对百度地图,pod开发与配置有太深入的研究,如有什么不对的地方,还请指出,我下来再专研专研。感谢!附上项目GitHub地址,也欢迎提issue和需求。