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("这里是中国的中心"));

这里有个小细节:titlesnippet 不是必须的,但如果你不加,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),那图标的左上角就会对准坐标点。我曾经因为没调锚点,导致标记看起来总是偏了一点,用户反馈说“你们这个定位不准啊”。其实不是定位不准,是图标没对齐。

小技巧:如果你用 SVG 或矢量图做图标,记得考虑不同屏幕密度的适配。建议准备多套尺寸,或者直接用 VectorDrawable。

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()

注意:InfoWindow 的点击事件只能通过 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 添加与自定义 addMarker() BitmapDescriptorFactory anchor 锚点设置 InfoWindow 定制 InfoWindowAdapter 接口 getInfoWindow() vs getInfoContents() ⚠️ 不可交互(静态快照) 相机移动 moveCamera() 瞬间跳转 animateCamera() 平滑动画 距离阈值判断(防抖动) 核心原则 标记要准(锚点)、窗口要美(自定义)、相机要稳(阈值) 三者配合,才能给用户流畅的地图体验

避坑指南

最后,我把自己踩过的坑整理了一下,你写代码时多留个心眼:

  • Marker 数量过多时性能下降——如果地图上有几百个标记,建议用聚类库(ClusterManager),否则地图会卡成 PPT。
  • InfoWindow 点击穿透——有时候点击 InfoWindow 会触发地图的点击事件。解决办法是在 InfoWindow 的点击监听里做 event.setHandled(true)
  • 相机动画被中断——如果你在动画还没结束时又调了一次 animateCamera(),前一个动画会被取消。可以用 CameraUpdateFactory.newLatLngZoom() 配合 animateCamera() 的 CancelCallback 来处理。
  • 位置权限问题——别忘了在 Android 6.0+ 动态申请权限。我曾经因为忘了处理权限拒绝的情况,导致位置回调一直为空,地图上的标记纹丝不动。

嗯,这一章的内容就这些。Marker 的添加和自定义、InfoWindow 的定制、以及相机跟随移动,这三个点搞明白了,地图交互这块你就掌握了七八成。剩下的,就是在实际项目里多练、多踩坑了。


公众号:蓝海资料掘金营,微信deep3321