简介:Android GIS开发中,地图初始化、图形绘制与离线数据加载是高频需求,而不同版本SDK的API差异常让开发者陷入迁移困境。ArcGIS for Android 100.5作为架构分水岭,以MapView、GraphicsOverlay等组件重新定义了地图应用构建方式,其官方示例代码涵盖坐标转换、符号渲染、图层查询等完整功能链路。通过剖析工程结构、核心API调用及生命周期管理,可以快速掌握从Gradle配置到真机调试的实践路径,并规避坐标偏移、黑屏、识别为空等常见问题。文章结合离线geodatabase与要素加载场景,帮助开发者将示例工程转化为可持续迭代的项目骨架,尤其适合从旧版10.x迁移或初次接触100.x架构的Android GIS工程师。
1. 为什么 100.5 这个版本值得你从头到尾跑一遍示例
做 Android 端 GIS 开发的人,多半都经历过这样的场景:项目工期紧,地图刚初始化完,又要加图形绘制、属性查询、离线切片,手忙脚乱地去翻官方文档、翻论坛、翻别人的博客,结果发现版本对不上,API 名字对不上,连包名都变了。ArcGIS for Android 100.5 正好处在一个分水岭上——它把旧的 10.x API 彻底甩开,全面转向 100.x 的新架构,异步任务、地理数据库、离线地图、符号渲染这些能力都变得比之前清晰得多。这个版本的完整示例代码,是很多人入门和迁移时真正踩过的路,我就是靠着它把项目从 10.2 迁过来的,也在里面踩过不少坑。
这篇文章要讲的,不只是把示例代码下载下来跑通,而是把它的工程结构、核心 API、坐标与符号、离线与查询、以及最容易翻车的地方,一个个拆开给你看。不管你是第一次接触 ArcGIS for Android,还是已经写了一阵子但总被某些报错卡住,这篇文章都能给你一条可复现的路径。尤其是那些在 100.5 里改了名字、换了写法的 API,网上资料零零散散,这里一次讲透。
2. 示例代码的工程构成与两种运行方式
2.1 用 Gradle 把工程拉起来之前,先搞清楚它里面有什么
ArcGIS for Android 100.5 的完整示例代码,官方发布形式是一个包含多个模块的 Android Studio 工程。常见做法是,代码库里既有arcgis-android-sdk-sample这样的主示例模块,也有独立的java和kotlin两种语言版本示例模块。这个设计很实在,因为当时很多团队还在 Java 和 Kotlin 之间摇摆,官方直接给两套,省得你自己翻译。
我第一次打开这个工程的时候,印象最深的是它的模块划分:每个示例都是一个独立的 Activity 或 Fragment,按照功能分类放在不同的包下面,比如display(地图显示)、analysis(分析)、geometry(几何)、features(要素)、layers(图层)、maps(地图操作)等等。这种组织方式的好处是,你想看哪个功能,直接进对应包,代码量小、依赖清晰,不像有些开源项目把几百个类堆在一起,翻半天找不到入口。
在跑起来之前,你需要确认两件事。第一,Android Studio 版本不能太低,100.5 的示例工程用的 Gradle Plugin 版本和 AGP 版本有一定要求,太老的 IDE 会直接同步失败;第二,SDK 的本地 Maven 仓库路径要配置对,因为 ArcGIS for Android 的 SDK 不是直接从 Google 的 Maven 中央仓库拉取的,而是通过 ArcGIS 自己的 Maven 仓库或者本地仓库文件来引用的。这一步如果配错,后面所有依赖都会标红。
配置本地仓库的常见写法如下,在项目根目录的build.gradle里添加:
allprojects { repositories { google() mavenCentral() maven { url 'https://esri.bintray.com/arcgis' } } }esri.bintray.com/arcgis是 100.5 时代官方指定的 Maven 仓库地址,SDK 的.aar包就是从这里面拉的。后来 Bintray 服务关停后,官方迁移到了新地址,但在 100.5 这个版本上,旧地址依然有效。如果你用新版本 SDK 的工程模板去套 100.5,这里就要改。我一般会先单独编译一个空项目来验证仓库可达性,再做示例工程导入,这样能把网络问题和工程问题分开排查。
2.2 用 Android Studio 导入示例代码,需要注意的四个细节
把示例代码跑起来,步骤不多,但细节不少。我直接在 Android Studio 里用Open选择示例工程根目录,等 Gradle 同步完成。这里有几个容易翻车的地方,我按踩坑顺序列给你。
第一个是 JDK 版本。100.5 的示例工程官方推荐 JDK 8,但你本机如果装的是 JDK 11 或更高,Gradle 同步有时候会报Unsupported class file major version之类的错误。这不是示例代码的问题,而是 Gradle 版本和 JDK 版本不匹配导致的。解决方法是把 Android Studio 里的 Gradle JDK 版本切到 8 或 11,具体看 Gradle wrapper 版本,我一般直接切到项目要求的版本。
第二个是 Android SDK 版本。示例工程里compileSdkVersion一般在 27 或 28 左右,你的本机如果没有安装对应版本的 SDK,Android Studio 会提示你下载。这个下载过程在国内网络环境下可能很慢,我用的是代理加手动下载 SDK 平台包的方式,避免卡在自动下载上。
第三个是签名问题。示例代码里很多功能,比如在线地图、地理编码、路径分析,都需要 API Key 才能跑。100.5 的示例代码里,有一个res/values/strings.xml或者assets目录下的配置文件会预留一个api_key字段,你需要去开发者后台申请 Key 填进去。如果不填,地图底图会加载失败,但矢量图形绘制这种本地功能不受影响。
第四个是模拟器还是真机。我建议直接用真机调试,因为 100.5 的地图渲染对 OpenGL ES 版本有要求,部分模拟器的显卡驱动不支持,会出现黑屏或者地图瓦片加载不出来。真机上虽然也会遇到问题,但至少环境更接近生产。
工程跑起来之后,你会在 Android Studio 的 Run 配置里看到很多个示例入口,选一个你关心的功能跑就行。比如我想看地图标注怎么加,就直接进GraphicsOverlay相关的示例,点运行,地图加载出来,标注显示在屏幕上,整个链路就通了。这一步通了,后面所有功能都能顺着这个链路去改。
3. 把 100.5 的示例代码拆开:核心 API 和功能模块的对应关系
3.1 地图初始化的新写法:MapView、ArcGISMap 和 Basemap
100.5 的地图初始化和旧版 10.x 完全是两套思路。旧版是MapView直接绑定一个ArcGISTiledMapServiceLayer,新版则引入了ArcGISMap这个顶层容器,底图、图层、初始视图都往里面塞。代码结构上,我一般这样写:
MapView mapView = findViewById(R.id.mapView); ArcGISMap map = new ArcGISMap(Basemap.Type.Topographic, 34.05, -118.25, 12); mapView.setMap(map);这里Basemap.Type.Topographic表示地形底图,34.05和-118.25是初始中心点经纬度,12是初始缩放级别。mapView 的生命周期管理很重要,100.5 里必须重写 Activity 的onResume、onPause和onDestroy,分别调用mapView.resume()、mapView.pause()和mapView.dispose()。
如果不调用这几个方法,地图在屏幕旋转或者页面切换时会出现内存泄漏和崩溃,尤其是dispose()漏掉后,反复进出页面,内存会一路涨上去,最后直接 OutOfMemory。示例代码里每个示例都写好了生命周期处理,这也是我建议你直接基于示例改而不是从空项目开始写的原因之一。
Basemap除了Topographic还有Streets、Imagery、Oceans等类型,你也可以用Basemap.fromUrl()加载自定义底图服务。我在实际项目里用的是自己发布的内网切片服务,写法就是Basemap.fromUrl("http://xxx/arcgis/rest/services/xxx/MapServer"),这样不依赖外网底图,在业务内网环境下也能正常跑。
3.2 图形绘制与 GraphicsOverlay:用 Graphic 表达点线面
地图上画点、画线、画面,在 100.5 里统一走GraphicsOverlay+Graphic+Symbol这套组合。以前 10.x 里GraphicsLayer的老写法已经废弃,迁移过来的人最容易在这里报错,因为找不到GraphicsLayer这个类了。先看一个画点的示例:
GraphicsOverlay graphicsOverlay = new GraphicsOverlay(); mapView.getGraphicsOverlays().add(graphicsOverlay); Point point = new Point(-118.25, 34.05, SpatialReferences.getWgs84()); SimpleMarkerSymbol symbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, 0xFFFF0000, 12); Graphic graphic = new Graphic(point, symbol); graphicsOverlay.getGraphics().add(graphic);GraphicsOverlay相当于一个承载图形对象的图层,Graphic是单个图形,Symbol决定图形长什么样。SimpleMarkerSymbol.Style.CIRCLE表示圆形,0xFFFF0000是红色 ARGB 颜色值,12是像素大小。如果你要画线,用Polyline配合SimpleLineSymbol;画面,用Polygon配合SimpleFillSymbol。这一套组合在 100.5 里非常稳定,性能也比 10.x 好很多,因为渲染引擎重写了,大量图形叠加时帧率不会掉得那么快。
我踩过的一个坑是坐标系。Point构造的时候如果不指定SpatialReference,默认是 Web 墨卡托,但你业务数据大多用的是 WGS84 经纬度,画出来位置会偏到海里。示例代码里大部分 Point 都显式传了SpatialReferences.getWgs84(),我后来自己写的时候也养成这个习惯。
3.3 图层加载:ArcGISMapImageLayer 与服务图层的区别
100.5 里的图层类型比 10.x 清晰很多。ArcGISMapImageLayer对应动态地图服务,ArcGISMapServerLayer对应要素服务,ArcGISTiledLayer对应切片服务。一开始我很不理解为什么要把图层类型拆这么细,后来明白了,不同类型的服务在服务端渲染方式、缓存策略、查询能力上都不一样,客户端拆分开才能在底层做针对性优化。
动态地图服务的加载示例:
ArcGISMapImageLayer imageLayer = new ArcGISMapImageLayer("http://xxx/arcgis/rest/services/xxx/MapServer"); map.getOperationalLayers().add(imageLayer);注意这里用的是getOperationalLayers(),也就是操作图层集合,底图是单独管理的。操作图层叠加在底图之上,可以控制透明度、可见范围、图例信息。在 100.5 里,图层的可见性控制比旧版方便很多,直接imageLayer.setVisible(false)就行,不会影响其他图层。
加载服务图层的时候最常遇到的问题就是服务地址不可达,报错信息一般是Layer failed to load。这个报错太笼统了,我通常会先用自己的网络工具确认地址能访问,再去看服务是否是 HTTPS 混合内容问题。Android 9 之后默认禁止 HTTP 明文流量,如果你的服务是http://开头,必须在AndroidManifest.xml里配置android:usesCleartextTraffic="true",或者用networkSecurityConfig单独放行。示例代码里的服务地址大多是https,但你自己换成本地服务时,这一步最容易漏。
3.4 属性查询与识别:Identify 操作在 100.5 里的正确姿势
在一张地图上点击要素查看属性,是 Web GIS 开发里最常见的需求。100.5 里对应的 API 是IdentifyLayerAsyncTask,示例代码里也有完整实现。它的基本逻辑是,把点击位置的屏幕坐标转成地图坐标,然后在地图坐标周围一定容差范围内,对指定图层做识别。
Point mapPoint = mapView.screenToLocation(screenPoint); IdentifyLayerAsyncTask task = new IdentifyLayerAsyncTask(imageLayer, mapPoint, 10, false, new IdentifyLayerAsyncTask.IdentifyLayerAsyncTaskListener() { @Override public void onCompleted(IdentifyLayerResult identifyLayerResult) { List<GeoElement> elements = identifyLayerResult.getElements(); for (GeoElement element : elements) { Map<String, Object> attributes = element.getAttributes(); // 遍历属性字典 } } @Override public void onError(Exception e) { // 处理识别失败 } }); task.execute();识别容差10表示屏幕像素的容差范围,值越大越容易命中,但也更容易误判。false表示只识别当前图层,不递归识别子图层。getAttributes()返回的是一个 Map,键是字段名,值是属性值,拿到之后可以直接填充到 UI 上。这个流程在 10.x 里是通过IdentifyTask异步完成的,100.5 里改成了IdentifyLayerAsyncTask,名字变了,参数顺序也变了,老代码迁移过来必须逐行改。
我还遇到过一个情况,就是识别结果一直为空,但图层数据确实存在。后来排查发现,是因为我没有等待图层完全加载完成就发起了识别请求,图层还在加载中,服务端自然识别不到。解决办法是在图层加载监听器的回调里再触发识别,或者先判断imageLayer.getLoadStatus() == LoadStatus.LOADED。
4. 坐标转换与符号渲染:示例代码里最容易看懂的参数设置
4.1 WGS84 与 Web 墨卡托的转换,以及你迟早要遇到的坐标系坑
ArcGIS for Android 100.5 底图默认是 Web 墨卡托(EPSG:3857),但业务数据大多是 WGS84(EPSG:4326)。如果你直接拿 WGS84 的坐标点去构造一个 Web 墨卡托空间参考下的 Graphic,显示位置会完全错乱。示例代码里的几何对象,绝大多数创建时都指定了SpatialReferences.getWgs84(),这样几何对象自身是 WGS84 坐标,MapView 会自动做投影转换渲染。
但有一种情况比较隐蔽:你从 XML 或者服务端接口拿到的坐标字符串,可能混用了坐标系,有的字段是 WGS84,有的已经是投影坐标。我通常的做法是,先统一规定后端返回的数据必须带SpatialReference信息,或者干脆全部使用 WGS84 经纬度。如果确实需要做转换,用GeometryEngine.project():
Point sourcePoint = new Point(x, y, SpatialReferences.getWgs84()); Geometry projectedGeometry = GeometryEngine.project(sourcePoint, SpatialReferences.getWebMercator());GeometryEngine.project()的第一个参数是源几何,第二个是目标空间参考。这个方法支持点、线、面等所有几何类型,内部做了坐标转换。我在做市政设施点位展示时,后端返回的是地方坐标系,我就是先用这个接口把坐标统一转成 WGS84,再往地图上加图标,才解决了位置偏移的问题。
4.2 符号类型与颜色参数:SimpleMarkerSymbol 和 PictureMarkerSymbol 怎么选
100.5 的符号系统分为两层:图层级符号(Renderer)和图形级符号(Symbol)。图形级符号又分很多具体类,最常用的是SimpleMarkerSymbol、SimpleLineSymbol、SimpleFillSymbol、PictureMarkerSymbol和TextSymbol。
SimpleMarkerSymbol是纯矢量绘制,点的大小、颜色、样式都是参数控制的。PictureMarkerSymbol是用本地图片或者 URL 图片作为图标,需要传图片的 Drawable 或者路径。实际项目里,业务图标大多是图片,比如警车、变电站、消防栓,用PictureMarkerSymbol更贴合需求。
我建议你优先用矢量符号做调试,因为颜色和大小改起来快,图片符号还要考虑尺寸适配,一张图在 mdpi 和 xxhdpi 屏上显示大小差很多。如果要用图片符号,代码里会写成:
PictureMarkerSymbol symbol = new PictureMarkerSymbol(getDrawable(R.drawable.fire_hydrant)); symbol.setHeight(36); symbol.setWidth(36); Graphic graphic = new Graphic(point, symbol);注意setHeight和setWidth的单位是像素,如果不设置,图片会按原始尺寸显示,有时候一张 200x200 的图片会把整个地图挡住。我一般在加载图片符号后强制设置宽高,避免低分辨率图片在地图上显示过大。
4.3 文本标注与偏移:TextSymbol 的 alignment 参数
地图上显示文字标注,TextSymbol是最直接的手段。示例代码里有一个专门展示文本标注的模块,核心参数是text、color、size、horizontalAlignment和verticalAlignment。我看到很多新手在这里翻车,文字位置总是偏的,因为默认对齐方式不是你想的那样。
TextSymbol textSymbol = new TextSymbol(16, "消防栓", 0xFF333333, TextSymbol.HorizontalAlignment.CENTER, TextSymbol.VerticalAlignment.MIDDLE); Graphic textGraphic = new Graphic(devicePoint, textSymbol);HorizontalAlignment.CENTER让文字水平居中于锚点,VerticalAlignment.MIDDLE让文字垂直居中于锚点。如果你想要文字显示在点位上方或下方,就调整这两个对齐参数。16是文字大小,单位是像素。字号在不同屏幕密度下的实际显示大小会有差异,我在平板上调试时会把字号调大一两档。
一个常见的需求是让文字和图标同时显示,比如消防救援点的图标上方再挂一个名称。这时候需要给图标和文字各建一个Graphic,放在同一个GraphicsOverlay里,位置错开。直接把文字偏移几个像素不现实,因为不同屏幕密度下偏移量不一样,更好的办法是使用Graphic的yOffset属性,单位是 DIP。
5. 避坑指南:100.5 示例代码从导入到上线的 5 个常见问题排查
5.1 地图黑屏或底图加载不出来,先查 API Key 和网络
现象:示例代码运行后,能看到 UI 控件,但地图区域一片黑或者空白的格子。
原因:最常见的是没有配置 API Key,或者配置了错误的 Key。其次,如果用的是 HTTP 明文服务,Android 9+ 默认禁止加载,日志里会出现Cleartext HTTP traffic not permitted的异常。再者,模拟器对 OpenGL 的支持不足也可能导致黑屏。
解决:第一步,检查strings.xml里的api_key是否填写正确,并且确认该 Key 对应的包名和签名指纹与当前调试应用一致。第二步,如果是 HTTP 地址,在AndroidManifest.xml的<application>标签上添加android:usesCleartextTraffic="true"。第三步,换到真机上运行,排除模拟器渲染问题。
5.2 Gradle 同步失败,版本号冲突第一反应不该是改依赖
现象:导入工程后 Gradle 同步一直失败,报错信息指向某个依赖包的版本不存在或者冲突。
原因:100.5 的示例工程在当时用的依赖版本是固定的,而新版本的 Android Studio 会自动尝试用最新的 Gradle Plugin 去解析,导致版本不兼容。还有一个可能是你的本机 SDK Manager 里没有对应版本的compileSdkVersion。
解决:先不要乱改依赖版本。看工程的gradle-wrapper.properties里指定的 Gradle 版本,再看根目录build.gradle里的 AGP 版本,以及app/build.gradle里的compileSdkVersion。三者之间必须匹配。我的做法是让 Android Studio 提示的 SDK 版本去下载,AGP 版本保持示例工程原值,除非报错明确指出必须升级。
5.3 识别结果为空,检查图层的加载状态
现象:地图上点击要素,回调正常执行,但getElements()返回的列表是空的。
原因:图层尚未加载完成就执行了识别,服务端没有数据可识别。还有一种可能,识别容差太小,点不够精确,目标要素的像素范围没被点击命中。
解决:先确认图层getLoadStatus()是否为LOADED,如果不是,注册LoadStatusChangedListener在加载完成后再绑定点击事件。另外把容差从 10 调到 20 或者 30 再试,排除点击偏差。如果仍然为空,回到服务端用 REST 接口测试识别是不是正常。
5.4 图形位置偏移,图形和底图不在同一个位置,先怀疑坐标系
现象:用 WGS84 坐标画了一个点,但点的位置显示在海里,或者相对底图整体偏移很远。
原因:构造Point时没有指定空间参考,默认按 Web 墨卡托处理。或者通过MapView.screenToLocation()得到的坐标是 WGS84,但你直接用成了服务坐标系。
解决:统一坐标系。调试时先用graphic.getGeometry().getSpatialReference()打出来看是什么坐标系,再决定是否需要GeometryEngine.project()。一般来说,后端数据统一转成 WGS84 经纬度再进客户端,客户端不转坐标,交给 MapView 做渲染转换,是最省事的。
5.5 页面退出后崩溃,MapView 生命周期没管好
现象:Activity 正常关闭,但日志报MapView is already disposed,或者过一会儿内存溢出崩溃。
原因:没有正确调用mapView.dispose(),或者在一个已经 dispose 的 MapView 上再次调用 resume / pause。
解决:严格按照示例代码的生命周期写法:
@Override protected void onResume() { super.onResume(); mapView.resume(); } @Override protected void onPause() { super.onPause(); mapView.pause(); } @Override protected void onDestroy() { super.onDestroy(); mapView.dispose(); }这里dispose()一定要在super.onDestroy()之后或者之前?我的习惯是放在super.onDestroy()之前,先把地图资源释放掉,再销毁 Activity 其他资源。如果用了 Fragmen 场景,MapView 的生命周期跟随 Fragment,需要额外注意重复创建时的资源释放。这一条是示例代码里做得最好的部分,照着抄就行了,不要自创。
6. 把示例代码改造成自己的项目骨架:三个值得保留的工程化习惯
示例代码看完了,坑也踩得差不多了,接下来最重要的事情是把它消化成自己项目的地基。我自己的经历是,把示例工程里的若干个功能模块抽取出来,组合成了一个小型地图应用骨架,后来在这个骨架上迭代了好几个项目,省掉了大量重复劳动。
第一个值得保留的习惯是FeatureLayer配合FeatureTable的方式加载本地或服务端要素。100.5 里,要素表可以从 ArcGIS Server 要素服务创建,也可以从本地 Geodatabase 创建。我经常用本地 Geodatabase 来做离线场景,在网络不通的时候,数据依然能查能画。关键代码是:
Geodatabase geodatabase = new Geodatabase("path/to/local.geodatabase"); FeatureTable featureTable = geodatabase.getGeodatabaseFeatureTableByName("fire_hydrant"); FeatureLayer featureLayer = new FeatureLayer(featureTable); map.getOperationalLayers().add(featureLayer);这段代码看起来简单,但离线数据的更新策略才是真正的重点。我一般会在应用启动时检查服务端数据版本,如果有更新,就把新的 Geodatabase 下载下来覆盖本地的。这个逻辑写起来不难,但它决定了离线功能能不能真正落地,而不是做成一个摆设。
第二个值得保留的习惯是SpatialReference穿透到底层数据模型。我在示例代码基础上封装的工具类里,所有坐标数据的输入输出都强制带上空间参考,不允许出现无坐标系的数据。这个强制要求在团队协作中尤其重要,不然每个新人都可能把坐标系搞混,地图效果就会非常混乱。从底层避免这类问题,比事后排查高效得多。
第三个习惯是把地图的 UI 状态和业务数据分离。示例代码里,GraphicsOverlay上的图形只是渲染层,真正的业务数据是放在自己的业务对象里的。点击一个图形,我用graphic.getAttributes()拿到业务 ID,再去数据源查详情。这种设计在项目大了以后非常有用,否则地图越来越复杂,图形和业务数据纠缠在一起,后期改一个字段都要在地图层面上处理半天。
还有一个很实用的技巧:在开发调试阶段,把示例工程自带的错误监听器保留,开发的时候日志输出得越详细越好。ArcGIS for Android 的很多错误信息是异步回调返回的,不挂监听器就看不到具体内容。等到上线前再把这些日志打成 release 不输出,既不影响调式体验,也避免了生产环境日志过多。
如果你正在基于 100.5 做项目,我强烈建议你花一两天时间把示例代码完整跑一遍,不要只跑你想用的那一个功能。很多 API 之间的调用逻辑是相通的,比如空间查询和属性查询的返回类型、图层加载状态的处理方式,用过一个功能再看另一个,理解成本会降低很多。我做迁移的那段时间,就是先跑Display map这个最基础的示例,再逐个看Feature layer query、Identify layer、Change symbol color,跑通三四个之后,整个 SDK 的套路就摸清楚了。希望这些踩过的坑和沉淀下来的习惯能帮到你,让你在 ArcGIS for Android 上少走弯路。
本文还有配套的精品资源,点击获取