ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Google地图API入门:从Demo跑通到工程集成避坑指南

Android Google地图API入门:从Demo跑通到工程集成避坑指南

简介:面向Android开发者的谷歌地图API入门级集成示例,完整演示从谷歌云控制台创建项目、启用地图SDK并获取密钥,到在清单文件中声明网络和定位权限、在布局文件中嵌入地图控件,再通过异步回调实现地图加载、相机移动到指定经纬度、打开定位按钮和添加标记点,适合刚接触地图服务的初学者快速搭建自己的第一个地图页面。资源以RAR压缩包发布,共27个文件,主体包含两个Java源码、三个XML布局与配置、九个编译生成的Class文件和六张PNG示例图片,同时附带混淆配置文件、工程属性文件以及一个可直接安装的APK;整个包体仅59KB,结构十分精简。目前已有567人学习下载,轻量且容易上手。MapsDemo2.2工程完整覆盖初始化、定位授权、相机移动与标记添加等关键步骤,开发者可逐文件阅读源码,结合附带APK观察运行效果,理清活动页面、布局文件与清单文件三者如何配合,为后续扩展路线规划、自定义图层和离线地图等高级功能打下扎实基础。

1. Android Google地图API Demo:跑得起来与接得进App是两回事

同样一个Android Google地图API Demo,有人十几分钟就能把它跑起来,有人卡在API Key配置上浪费一整天,区别不在代码水平,而在于有没有把这条链路理顺。这个Demo表面看是一个能显示地图的页面,实际牵扯到SDK依赖、AndroidManifest里的元数据、签名指纹、运行时授权,以及模拟器上根本不存在的GPS硬件。这份资源适合已经写过Android、想把地图能力接进自己App但不想把一整个下午耗在文档里的从业者。把它跑通,你就能看到地图初始化、Marker、相机移动和定位图层最基础的写法,后面接业务需求,只是在这套骨架上长肉。

2. 工程骨架与Key配置:依赖、清单、SHA-1三件套

2.1 为什么选Google Maps SDK而不是自己画地图

早期做过地图类的从业者可能都想过:我能不能自己画一张地图?用瓦片图片、自己写缩放和手势、把经纬度换算成屏幕坐标。实际这么做的人很快会发现,坐标投影、瓦片加载策略、手势冲突、标注聚合,每一个都是要填的坑,而且填完之后往往不如官方SDK稳定。

Google Maps SDK for Android解决的不是“显示一张图片”,而是把地图渲染、坐标系换算、手势交互、标注图层这些基础设施封装好。你只需要传入WGS-84坐标系下的经纬度,SDK内部会完成投影换算和瓦片加载。从坐标到屏幕位置的换算不需要关心,从屏幕点击反查经纬度也不需要关心,这些都是SDK的黑匣子。

Demo里选择SDK而不是自研,对应的选型理由有三个:第一,瓦片数据源和离线策略已经内置,不用自己维护;第二,Marker、Polyline、GroundOverlay这些图层对象是现成的数据模型,和业务数据对接方便;第三,批量接入成本低,一个依赖库加一个API Key就能起步。

2.2 工程搭建:build.gradle、AndroidManifest、布局XML

先看依赖。在app模块的build.gradle里加上Maps SDK的依赖,我用的是play-services-maps。minSdk建议21起,targetSdk按你自己业务定,通常不低于30。

dependencies { implementation 'com.google.android.gms:play-services-maps:18.2.0' }

这段依赖会把地图核心库、瓦片加载器、地图控件一起打进来,不需要再单独引其它地图相关包。版本号18.2.0是稳定的一个系列,如果你的项目里已经有play-services全家桶,注意保持一致,否则可能出现类冲突。

接着在AndroidManifest.xml里声明网络权限和API Key元数据。API Key在这里不是写在BuildConfig里,而是以meta-data形式挂在application节点下,这是SDK规定的读取路径。

<uses-permission android:name="android.permission.INTERNET" /> <application> <meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_API_KEY" /> </application>

注意meta-data的name不能拼错,value不能为空,位置必须在application节点内部。SDK启动的时候会从这里读Key,读不到就报错白屏。用BuildConfig或代码里setApiKey的方式不会生效,这是很多新手踩的第一个坑。

布局层面,Demo最常用的方式是使用SupportMapFragment,它可以单独放在XML布局里,也能在代码里动态添加。

<fragment android:id="@+id/map" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" />

注意name用SupportMapFragment而不是MapFragment。后者是旧版API,在androidx环境下会有兼容问题,这也是后面onMapReady不回调的常见原因。

2.3 API Key与SHA-1:整个Demo最容易翻车的地方

API Key的获取不算难,难的是把Key和你的App绑定正确。控制台里创建Key的时候,要求填写包名和一个SHA-1证书指纹,这个指纹必须和你本地打包的签名证书一致。

调试阶段用的是默认的debug.keystore,在用户目录下的.android文件夹里。获取指纹的命令在Windows和macOS/Linux下略有差异,但本质都是调keytool:

keytool -list -v \ -keystore ~/.android/debug.keystore \ -alias androiddebugkey \ -storepass android \ -keypass android

输出里找到SHA1字段,复制那一串带冒号的十六进制字符串,粘贴到控制台对应的位置。这里不要用SHA256,Maps SDK的Key绑定校验目前读的是SHA1。

绑定完之后,把Key填到manifest里,然后运行Demo。如果一切正常,地图就出来了。如果白屏,先看Logcat,通常会看到The Google Maps API key is invalid的日志,那就按三个方向排查:包名是否和控制台上填的一致、SHA1指纹是否来自当前签名文件、该Key是否启用了Maps SDK服务。

有一个容易忽略的点:拿到Key并绑好debug指纹之后,Debug构建没问题,但如果用Release签名打包,必须把Release证书的SHA1也加到控制台同一个应用条目下,否则Release包一打开地图就是白的。这种“debug正常、release翻车”的问题在交付给客户的时候最容易出现,后面第4章会详细展开。

3. 地图核心对象拆解:Marker、Camera与定位相关权限

3.1 onMapReady回调链:地图不是同步创建的

写地图代码的人第一眼看到的通常是onMapReady这个回调。表面上看,它只是告诉我们“地图准备好了”,但它背后是一条异步链路:Activity创建布局、SupportMapFragment关联到GoogleMap对象、地图核心引擎初始化瓦片渲染器,这些都在不同时机完成。

Demo里最常见的写法是在Activity里拿到SupportMapFragment,然后调用getMapAsync:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val mapFragment = supportFragmentManager .findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this::onMapReady) } private fun onMapReady(map: GoogleMap) { map.uiSettings.isZoomControlsEnabled = true map.uiSettings.isCompassEnabled = true } }

getMapAsync的回调并不保证发生在onCreate之后立刻执行,它可能发生在布局完成后的某一帧。所以不能在getMapAsync之前就调用map.addMarker,否则会拿到一个空引用。

参数说明两条最常用的UI设置:isZoomControlsEnabled控制屏幕右下角的加减号按钮,isCompassEnabled控制左上角的方向标。如果你做的是纯展示型页面,这两个都可以关掉,减少遮挡。

3.2 Marker与Camera:把数据落到地图上的两种操作

Marker是这个Demo里最高频的对象。一个坐标点想变成地图上的图标,需要构造MarkerOptions再传递给addMarker。这里有几个参数直接影响最终显示效果:

val point = LatLng(31.2304, 121.4737) map.addMarker( MarkerOptions() .position(point) .title("演示点") .snippet("这个点用来验证Marker") .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)) .anchor(0.5f, 1f) .draggable(true) ) map.animateCamera(CameraUpdateFactory.newLatLngZoom(point, 16f))

position传的是WGS-84经纬度,title和snippet会在点击Marker时以小气泡形式显示。icon默认是一个红色水滴形倒锥,HUE_RED是默认色相,也可以用HUE_BLUE、HUE_GREEN或者用BitmapDescriptorFactory.fromResource传自定义图标。

anchor参数容易被忽略。它定义图标的哪个位置对应坐标点。默认是图标底部中心,也就是anchor(0.5f, 1f)。如果用自定义图标,比如一个圆形定位按钮,你可能希望改成anchor(0.5f, 0.5f),让圆心对准坐标。参数里第一个是水平比例,第二个是垂直比例,取值范围0到1。

CameraUpdateFactory负责生成相机位置变化,newLatLngZoom接受经纬度和缩放级别。缩放级别范围一般从2(世界视野)到21(建筑级别),16左右能看清街道。animateCamera会做平滑移动,moveCamera直接跳转,业务上体验优先用前者。

3.3 定位权限与图层:什么时候问用户要权限

打开定位蓝点需要设置isMyLocationEnabled,但这个设置之前必须确定权限已经授予。Android 6之后运行时权限是没法绕过的,Demo里需要一个小的权限判断:

private fun enableLocation(map: GoogleMap) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val fine = ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) if (fine != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQ_LOCATION ) return } } map.isMyLocationEnabled = true }

Manifest里还需要声明对应权限:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

FINE和COARSE的区别是精度范围。只声明COARSE的话,定位蓝点会经常漂到街区级别;只声明FINE而在部分机型上可能被系统识别为强定位而引导用户二次确认。Demo代码里直接申请FINE,COARSE一起声明但不用额外逻辑,这是最省事的做法。

权限回调里没有自动重试逻辑,所以处理完结果后要手动再调用一次enableLocation。很多开发者在这个环节漏掉,用户授权后蓝点依然不出现。

4. 白屏、不回调、定位漂移:四类高频问题排查记录

4.1 白屏:只看到网格看不到地图瓦片

现象:地图区域只有底色和网格线,没有任何瓦片,Logcat里能看到带页面地址的请求报错或直接打出一行API key invalid的提示。

原因:这类白屏九成是API Key校验失败。拆开看有三个具体子原因:一是控制台里Key绑定的包名和当前运行的App包名不一致;二是绑定的是Release签名指纹,但本地跑的是Debug构建;三是Key在控制系统里没有启用Maps SDK服务,创建Key的时候默认可能只开了部分服务。

解决:先把Logcat里的错误信息完整读一遍,不要只看第一行。然后按顺序核对:当前运行包名、当前构建类型的签名指纹、控制台里Key绑定的服务列表。我一般会用另一个空白工程单独跑同一个Key,如果空白工程能出地图,说明问题在宿主App的配置上,不在Key本身。

额外提醒:不要把API Key打印在日志里,也不要提交到Git仓库。Key一旦泄露,对方只需要拿到你的包名和签名指纹就能伪装调用。控制台里不要创建带通配权限的受限Key。

4.2 onMapReady不回调:拿到地图对象的时机不对

现象:Activity正常启动,地图区域一片空白,onMapReady里设的缩放、Marker完全没执行,也没有任何异常日志。

原因:不是地图SDK坏了,而是SupportMapFragment没有正确挂载到Activity。常见情况是从网上搬了一段旧代码,XML里用了MapFragment而不是SupportMapFragment,但Activity继承的是AppCompatActivity,两者来自不同的Fragment体系,SDK的getMapAsync找不到对应的实例。另一个原因是在Fragment内部再嵌套SupportMapFragment,但没有用getChildFragmentManager去管理。

解决:统一使用androidx下的AppCompatActivity配合SupportMapFragment。在Activity中用supportFragmentManager.findFragmentById获取Fragment实例。如果是在Fragment里嵌地图,需要使用childFragmentManager而不是requireActivity().supportFragmentManager。还有一个习惯:先把Fragment是否存在判断好,为空时就不要调用getMapAsync。

4.3 模拟器上定位偏离:蓝点永远不在当前位置

现象:真机上运行正常,换成模拟器后蓝点要么不显示,要么出现在一个离坐标很远的位置,看起来像“漂移”。

原因:模拟器没有真实的GPS硬件,系统提供的LocationProvider在无外部数据的情况下只能返回一个默认位置,Demo代码读到的是这个空心坐标,不是真实经纬度。

解决:在模拟器的Extended Controls里找到Location面板,手工输入一组经纬度后点击Send,然后重启地图页面。如果需要自动化测试,可以在代码里用LocationManager的setTestProviderLocation注入坐标,但Demo阶段用模拟器面板最省事。

真实设备上遇到定位不准,先确认是否在室内,靠近窗户会好一些;再确认授予的是精确位置权限;最后确认系统里没有打开省电模式。这些因素单个看都正常,合在一起就会变成“玄学飘移”。

4.4 Release包白屏:debug正常,换签名就废

现象:Debug构建跑得好好的,打Release包发给别人测试,对方一打开地图就是空白。

原因:控制台里的Key绑定的是debug.keystore的SHA1指纹。Release包用的正式签名文件是另一套证书,SDK校验指纹时不匹配,就拒绝加载地图。

解决:把Release签名对应的SHA1也添加到控制台的应用条目下,建议同时保留Debug和Release两个指纹。以后一旦换了签名文件或者用自动签名服务,记得同步更新控制台。从那以后我每次发版前都会先检查一遍当前构建用的签名指纹跟控制台是否匹配,这个检查已经变成了例行动作。

5. 进阶验证:批量Marker与地理围栏判断的一个可执行样本

5.1 批量加Marker时的渲染取舍

跑通Demo之后,下一个自然需求是把业务数据点标到地图上。批量添加Marker的代码并不复杂:

val points = listOf( LatLng(31.2304, 121.4737), LatLng(31.2400, 121.4830), LatLng(31.2500, 121.4930) ) points.forEachIndexed { index, latLng -> googleMap.addMarker( MarkerOptions() .position(latLng) .title("点$index") .icon(BitmapDescriptorFactory.defaultMarker(index * 30f)) ) }

这段代码把三个坐标点标成不同颜色的Marker,title里的index用来区分。注意index * 30f是色相值,HUE在0到360之间,所以三个点会偏红、偏绿、偏橙。这里有个取舍:当Marker数量超过五十个时,逐个addMarker会开始掉帧,因为每一次addMarker都会触发图层重绘。知乎上有人测试过一百个点的时候已经能感觉到滑动不流畅,这时候需要引入ClusterManager做聚合,或者限制当前缩放级别下只渲染视野内的点。

demo数据量小的时候不用考虑这些,一旦接到真实业务数据,优先做聚合再做样式。

5.2 地理围栏验证:一个不需要后端就能跑的思路

最后一个技巧是用Marker坐标做简单的圆形围栏判断。假设你已经知道业务点的经纬度,想判断它是否在某个范围附近,可以用Location.distanceBetween计算测地线距离:

fun isInsideCircle( point: LatLng, center: LatLng, radius: Float ): Boolean { val results = FloatArray(1) Location.distanceBetween( point.latitude, point.longitude, center.latitude, center.longitude, results ) return results[0] <= radius }

distanceBetween基于WGS-84椭球模型计算两点间距离,比直接用经纬度差的勾股定理要准确得多。这个函数在Demo里可以挂在Marker点击事件上,点击某个点立刻判断它是否在你预设的配送范围或业务范围内。更复杂的多边形围栏需要用到GeofencingApi,但那个要处理Intent传输和生命周期注册,Demo阶段用这个简单版本已经足够。

从那以后我每次做地图功能,都会先用这个Demo把Key校验、权限申请、Marker渲染这条链路完整走一遍,再动手写业务代码。这份资源最大价值不是那一百多行代码,而是把Google地图SDK的配置链路压缩成一个可重复跑的样本。希望你拿到之后也能顺畅跑通,少踩几个我已经替你踩过的坑。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进