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
相关产品推荐
相关产品推荐

