22、位置标记:Marker添加与自定义、InfoWindow定制、位置更新时移动地图相机
地图上光有定位点,说实话有点单调。你想想看,用户打开地图,看到的只是一个蓝色小圆点,那跟看一张白纸有什么区别?真正让地图“活”起来的,是那些标记——Marker。
这一章,我们就来聊聊怎么往地图上扔标记,怎么让标记长得不一样,怎么给标记加个“气泡对话框”,以及当用户位置变化时,怎么让相机乖乖跟着跑。
Marker 的基本添加
添加一个 Marker 其实很简单。你只需要一个 LatLng 坐标,然后调用 addMarker() 方法就行。我刚开始做地图功能时,觉得这步太基础了,结果第一次上线就出了 bug——标记加到了南极洲。嗯,坐标写反了。
// 创建一个标记,放在北京天安门
LatLng beijing = new LatLng(39.9042, 116.4074);
mMap.addMarker(new MarkerOptions()
.position(beijing)
.title("天安门广场")
.snippet("这里是中国的中心"));
这里有个小细节:title 和 snippet 不是必须的,但如果你不加,InfoWindow 就弹不出来。说白了,这两个字段就是给 InfoWindow 提供内容的。
自定义 Marker 样式
默认的红色图钉,说实话看多了会腻。而且不同业务场景需要不同的图标——比如咖啡店用咖啡杯图标,加油站用油枪图标。我个人习惯把图标资源放在 res/drawable 目录下,然后通过 BitmapDescriptorFactory 加载。
// 自定义图标
mMap.addMarker(new MarkerOptions()
.position(new LatLng(39.9042, 116.4074))
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_custom_marker))
.anchor(0.5f, 1.0f) // 设置锚点,底部居中
.title("自定义标记"));
这里有个坑,我踩过好几次:锚点(anchor)。默认锚点是 (0.5, 1.0),也就是图标底部中心对准坐标点。如果你改成 (0, 0),那图标的左上角就会对准坐标点。我曾经因为没调锚点,导致标记看起来总是偏了一点,用户反馈说“你们这个定位不准啊”。其实不是定位不准,是图标没对齐。
InfoWindow 定制
InfoWindow 就是点击 Marker 后弹出的那个小气泡。默认样式很简陋,就一个白底黑字。如果你想让它长得好看点,就得自己定制布局。
定制 InfoWindow 需要实现 GoogleMap.InfoWindowAdapter 接口。我当年第一次做这个,折腾了一下午,因为有个坑:InfoWindow 不是 View,它是一张图片。什么意思呢?就是你没法在 InfoWindow 里放按钮、输入框这些可交互的控件。它只是一个静态的快照。
mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
@Override
public View getInfoWindow(Marker marker) {
// 返回 null 表示使用默认窗口
return null;
}
@Override
public View getInfoContents(Marker marker) {
// 自定义内容视图
View view = getLayoutInflater().inflate(R.layout.custom_info_window, null);
TextView title = view.findViewById(R.id.info_title);
TextView snippet = view.findViewById(R.id.info_snippet);
title.setText(marker.getTitle());
snippet.setText(marker.getSnippet());
return view;
}
});
注意看,getInfoWindow() 和 getInfoContents() 的区别:前者是整个窗口的样式,后者只是内容区域的样式。如果你只重写 getInfoContents(),那窗口的外框还是默认的白色圆角矩形。如果你想完全自定义,包括背景、边框、阴影,那就得重写 getInfoWindow()。
setOnInfoWindowClickListener() 来监听。你没法在自定义布局里加 Button 并设置 OnClickListener——因为那只是一张图片,不是真正的 View 树。
位置更新时移动地图相机
这是地图类 App 最核心的交互之一:用户位置变了,地图要跟着动。你不能让用户每次移动都手动去拖地图,那体验太差了。
实现方式很简单:在位置回调里,调用 moveCamera() 或 animateCamera()。区别在于,moveCamera() 是瞬间跳转,animateCamera() 有平滑动画。
@Override
public void onLocationChanged(Location location) {
LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());
// 平滑移动相机
CameraUpdate update = CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f);
mMap.animateCamera(update);
// 更新标记位置(如果有的话)
if (myMarker != null) {
myMarker.setPosition(currentLatLng);
}
}
这里有个性能问题:不要每次位置更新都调用 animateCamera()。位置回调的频率可能很高(比如每秒一次),如果你每次都做动画,地图会一直“抖”个不停,用户看着头晕。我建议加一个距离阈值判断:只有当位置变化超过一定距离(比如 10 米)时,才移动相机。
// 距离阈值判断
float[] results = new float[1];
Location.distanceBetween(
lastLat, lastLng,
currentLat, currentLng,
results
);
if (results[0] > 10) { // 超过10米才移动
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(currentLatLng, 15f));
lastLat = currentLat;
lastLng = currentLng;
}
这样做的好处很明显:既保证了地图跟随的流畅性,又避免了无谓的动画开销。我曾经在一个骑行 App 里用过这个方案,用户反馈说“地图跟得很稳,不晃眼”。
知识体系总览
下面这张图,我把本章的核心知识点串了一下。你可以把它当作一个快速索引,复习时瞄一眼就行。
避坑指南
最后,我把自己踩过的坑整理了一下,你写代码时多留个心眼:
- Marker 数量过多时性能下降——如果地图上有几百个标记,建议用聚类库(ClusterManager),否则地图会卡成 PPT。
- InfoWindow 点击穿透——有时候点击 InfoWindow 会触发地图的点击事件。解决办法是在 InfoWindow 的点击监听里做
event.setHandled(true)。 - 相机动画被中断——如果你在动画还没结束时又调了一次
animateCamera(),前一个动画会被取消。可以用CameraUpdateFactory.newLatLngZoom()配合animateCamera()的 CancelCallback 来处理。 - 位置权限问题——别忘了在 Android 6.0+ 动态申请权限。我曾经因为忘了处理权限拒绝的情况,导致位置回调一直为空,地图上的标记纹丝不动。
嗯,这一章的内容就这些。Marker 的添加和自定义、InfoWindow 的定制、以及相机跟随移动,这三个点搞明白了,地图交互这块你就掌握了七八成。剩下的,就是在实际项目里多练、多踩坑了。