You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Java 谷歌地图自定义InfoWindow结合Firebase加载标记数据崩溃问题

问题根因

当前代码崩溃、标记对应数据加载错位的核心原因有4个:

  • 错误地将全局配置项InfoWindowAdapter放到标记点击事件里反复初始化:这个适配器是作用于整个地图实例的,不是点哪个标记就替换一次。你每次点击都触发新的Firebase异步请求,请求未返回时系统已经开始渲染信息窗,轻则数据对不上,重则空指针崩溃。
  • 标记与业务数据完全解绑:添加标记时仅存入了title字段,剩余图片、车位数量等数据完全没有绑定到标记上,点击后再靠title二次查询Firebase,不仅多了一层无意义的异步开销,title重复时还会匹配到错误数据。
  • 适配器全局复用同一个View实例:所有标记的信息窗都用同一个mWindows对象渲染,极易出现数据串位、视图状态错乱的问题。
  • 缺失空值校验:Firebase返回的字段如果为空,直接调用toString()、拆箱基础类型会直接触发空指针崩溃。
正确实现方案

第一步:定义点位数据实体类,初始化时直接把数据绑定到标记上

不要等点击标记再查库,首次拉取Location集合时就把所有数据解析完成,直接存到对应Marker的tag字段中,省掉二次异步请求。

// 存点位业务数据的实体类
public class LocationItem {
    public String title;
    public String pic;
    public long bike;
    public long parking;
    public GeoPoint gps;

    public LocationItem(String title, String pic, long bike, long parking, GeoPoint gps) {
        this.title = title;
        this.pic = pic;
        this.bike = bike;
        this.parking = parking;
        this.gps = gps;
    }
}

第二步:修改onMapReady逻辑,全局仅设置一次信息窗适配器

不要在标记点击回调里反复替换适配器,地图初始化完成后设置一次即可,拉取到点位数据后直接加标记、绑定数据。

@Override
public void onMapReady(GoogleMap googleMap) {
    mMap = googleMap;
    mMap.getUiSettings().setZoomControlsEnabled(true);
    // 全局只初始化一次适配器,不要反复new
    mMap.setInfoWindowAdapter(new MyInfoWindowsAdapter(MapsActivity.this));

    db.collection("Location")
            .get()
            .addOnCompleteListener(task -> {
                if (task.isSuccessful() && task.getResult() != null) {
                    LatLng lastLatLng = null;
                    for (QueryDocumentSnapshot doc : task.getResult()) {
                        // 所有字段做空校验,避免后台漏填字段触发崩溃
                        String title = doc.getString("title") == null ? "" : doc.getString("title");
                        String pic = doc.getString("pic") == null ? "" : doc.getString("pic");
                        Long bikeNum = doc.getLong("bike");
                        long bike = bikeNum == null ? 0 : bikeNum;
                        Long parkingNum = doc.getLong("parking");
                        long parking = parkingNum == null ? 0 : parkingNum;
                        GeoPoint gps = doc.getGeoPoint("GPS");
                        if (gps == null) continue; // 没有坐标的点位直接跳过,不添加标记
                        
                        LatLng latLng = new LatLng(gps.getLatitude(), gps.getLongitude());
                        LocationItem item = new LocationItem(title, pic, bike, parking, gps);
                        // 添加标记时直接把对应业务数据存在tag中
                        Marker marker = mMap.addMarker(new MarkerOptions()
                                .position(latLng)
                                .title(title));
                        if (marker != null) {
                            marker.setTag(item);
                        }
                        lastLatLng = latLng;
                    }
                    if (lastLatLng != null) {
                        CameraPosition cameraPosition = new CameraPosition.Builder()
                                .target(lastLatLng)
                                .zoom(15)
                                .build();
                        mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
                    }
                }
            })
            .addOnFailureListener(e -> Log.d("Demo", "点位拉取失败: " + e.getMessage()));

    // 不需要额外写setOnMarkerClickListener,默认点击逻辑就会触发信息窗渲染,除非你有其他点击交互需求
}

第三步:重写信息窗适配器,从当前标记的tag中取数据渲染

不要把点位数据写死在适配器的成员变量里,每次渲染信息窗时,从当前触发的Marker对象的tag字段中取对应业务数据,彻底解决数据串位问题。

public class MyInfoWindowsAdapter implements GoogleMap.InfoWindowAdapter {
    private final Context mContext;
    private final StorageReference storageRef = FirebaseStorage.getInstance().getReference();

    public MyInfoWindowsAdapter(Context context) {
        this.mContext = context;
    }

    @Nullable
    @Override
    public View getInfoWindow(@NonNull Marker marker) {
        // 每次渲染都inflate新的视图实例,不要复用全局View避免状态错乱
        View infoView = LayoutInflater.from(mContext).inflate(R.layout.info_windows, null);
        renderWindowContent(marker, infoView);
        return infoView;
    }

    @Nullable
    @Override
    public View getInfoContents(@NonNull Marker marker) {
        // 和getInfoWindow二选一实现即可,不需要两个方法都写渲染逻辑
        return null;
    }

    private void renderWindowContent(Marker marker, View view) {
        LocationItem item = (LocationItem) marker.getTag();
        if (item == null) return;

        TextView tvTitle = view.findViewById(R.id.tvTitle);
        TextView tvBike = view.findViewById(R.id.tvBike);
        TextView tvParking = view.findViewById(R.id.tvParking);
        ImageView ivPic = view.findViewById(R.id.iV1);
        ImageView bikeIcon = view.findViewById(R.id.imageView);
        ImageView parkingIcon = view.findViewById(R.id.imageView2);

        tvTitle.setText(item.title);
        tvBike.setText(String.valueOf(item.bike));
        tvParking.setText(String.valueOf(item.parking));
        bikeIcon.setImageResource(R.drawable.bike);
        parkingIcon.setImageResource(R.drawable.parking);
        // 先设置默认占位图
        ivPic.setImageResource(R.drawable.a2);

        // 加载点位对应图片
        if (!TextUtils.isEmpty(item.pic)) {
            storageRef.child("imgs_stust").child(item.pic + ".jpg")
                    .getDownloadUrl()
                    .addOnSuccessListener(uri -> Glide.with(mContext)
                            .load(uri)
                            .placeholder(R.drawable.a2)
                            .listener(new RequestListener<Drawable>() {
                                @Override
                                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                                    return false;
                                }

                                @Override
                                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                                    if (marker.isInfoWindowShown()) {
                                        marker.hideInfoWindow();
                                        marker.showInfoWindow();
                                    }
                                    return false;
                                }
                            }).into(ivPic))
                    .addOnFailureListener(e -> Log.d("Demo", "图片加载失败: " + e.getMessage()));
        }
    }
}
额外优化点
  • 之前写的setinfowindow方法可以直接删除,完全没有存在的必要,反而会导致适配器反复替换引发异常。
  • 即使存在title重名的点位也不会出现数据匹配错误,因为每个标记的业务数据是直接绑定在自身tag上的,不依赖title做匹配。
  • 图片加载完成后刷新信息窗的逻辑可以保留,这是谷歌地图信息窗加载网络图片的标准处理方案。

内容的提问来源于stack exchange,提问作者Louis lin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:15:34