☰
Google Maps V2 信息窗口加载网络图片:cw-omnibus 的 ImagePopups 示例深度解析
2026/10/16 2:31:00 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

Source code to omnibus edition of _The Busy Coder's Guide to Android Development_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载

本指南以 cw-omnibus 仓库中 MapsV2/ImagePopups/README.markdown 所描述的示例应用为主线,完整解析如何在 Google Maps V2(play-services-maps)的 Marker 信息窗口(Info Window)中异步加载并展示网络图片。读者将掌握InfoWindowAdapter的自定义实现、Picasso 异步图片加载与信息窗口内容刷新的配合方式,以及 Maps V2 应用必备的 Google Play 服务兼容性检查流程,可直接将这套模式复用到自己的地图类应用中。

一、示例应用要解决什么问题

官方文档对 ImagePopups 的定位只有一句话:演示包含下载图片的信息窗口("info windows containing downloaded images")。其背后对应的是 Maps V2 开发中一个常见且棘手的需求——默认的 Marker 信息窗口只支持标题(title)和摘要(snippet)两行文字,无法直接渲染图片、自定义布局。

本示例正是围绕这一点展开:通过实现GoogleMap.InfoWindowAdapter接口,把信息窗口的内容替换为自定义视图,并在该视图中使用 Picasso 异步加载存放在网络上的建筑照片,同时处理图片尚未加载完成时信息窗口已绘制成快照的问题。全部源码位于 MapsV2/ImagePopups/app/src/main 下,共三个 Java 类与若干布局、资源文件,结构非常精简,适合作为学习起点。

1.1 项目结构与运行环境

文件作用
MainActivity.java地图初始化、添加 Marker、注册信息窗口适配器
PopupAdapter.java实现InfoWindowAdapter,用 Picasso 异步加载图片
AbstractMapActivity.javaGoogle Play 服务可用性检查基类,可复用
activity_main.xml仅含一个SupportMapFragment
popup.xml信息窗口自定义布局(图片 + 标题 + 摘要)
AndroidManifest.xml权限、API Key 与 OpenGL ES 特性声明
app/build.gradle依赖与构建配置

从 app/build.gradle 可见该示例的技术栈:

dependencies { implementation 'com.android.support:support-v4:27.1.1' implementation 'com.google.android.gms:play-services-maps:15.0.1' implementation 'com.squareup.picasso:picasso:2.5.2' } android { compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 targetSdkVersion 27 } }
  • minSdkVersion 14:覆盖 Android 4.0 及以上设备;
  • play-services-maps 15.0.1:Maps V2 的 Google Play 服务组件,提供GoogleMap、SupportMapFragment等核心 API;
  • Picasso 2.5.2:Square 的图片加载库,负责网络图片的异步拉取、缩放与缓存;
  • support-v4 27.1.1:提供FragmentActivity与SupportMapFragment的兼容支持。

另外仓库还保留了一份旧式 Ant 构建的 project.properties(target=android-19,并引用google-play-services_lib),仅用于兼容早期构建流程,实际开发建议以 Gradle 配置为准。

1.2 运行前提

运行本示例需要满足:

  1. 设备/模拟器上已安装可用的 Google Play 服务(Maps V2 依赖其运行);
  2. 具备 OpenGL ES 2.0 及以上能力(AndroidManifest.xml中通过uses-feature声明);
  3. 替换 AndroidManifest.xml 中的com.google.android.maps.v2.API_KEY为你自己在 Google Cloud Console 申请的地图密钥(示例中硬编码的AIzaSyC4iyT46cB00IdKGcy5EmAxK5uCOQX2Oy8仅为示例代码的演示密钥,正式发布前必须更换)。

二、自定义信息窗口:InfoWindowAdapter 的两种方法

Maps V2 为每个 Marker 提供默认信息窗口,仅能显示两行文本。要突破这个限制,需要调用map.setInfoWindowAdapter()注册适配器。PopupAdapter 实现了GoogleMap.InfoWindowAdapter接口:

class PopupAdapter implements InfoWindowAdapter { private View popup=null; private LayoutInflater inflater=null; private HashMap<String, Uri> images=null; private Context ctxt=null; private int iconWidth=-1; private int iconHeight=-1; private Marker lastMarker=null; PopupAdapter(Context ctxt, LayoutInflater inflater, HashMap<String, Uri> images) { ... iconWidth = ctxt.getResources().getDimensionPixelSize(R.dimen.icon_width); iconHeight = ctxt.getResources().getDimensionPixelSize(R.dimen.icon_height); } @Override public View getInfoWindow(Marker marker) { return(null); } @Override public View getInfoContents(Marker marker) { ... } }

接口中的两个方法分工明确:

方法返回值语义
getInfoWindow(Marker)返回整个信息窗口(含边框、背景)的自定义视图;返回null表示沿用地图默认的窗口外观
getInfoContents(Marker)仅替换默认窗口内部的内容区域,外观(圆角背景、小尾巴箭头)仍由地图引擎绘制

本示例的策略是getInfoWindow返回null、getInfoContents返回自定义布局——这样可以保留信息窗口原生的小尾巴与圆角阴影,同时自由定制内部排版。PopupAdapter在构造时即从 dimens.xml 读取图片的目标尺寸:

<dimen name="icon_width">96dp</dimen> <dimen name="icon_height">64dp</dimen>

这两个值会用于后续 Picasso 的resize(iconWidth, iconHeight)调用,避免直接把原图塞进信息窗口导致布局溢出。

三、MainActivity:Marker 与图片 URL 的映射

MainActivity 继承AbstractMapActivity(FragmentActivity子类),同时实现OnMapReadyCallback与OnInfoWindowClickListener。布局文件 activity_main.xml 只有一个全屏的SupportMapFragment:

<fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" class="com.google.android.gms.maps.SupportMapFragment"/>

在onCreate中通过mapFrag.getMapAsync(this)异步获取GoogleMap实例(API 15.0.1 的标准用法),并用savedInstanceState == null判断是否为首次进入,从而决定是否需要初始化相机位置。

3.1 相机初始化与 Marker 添加

onMapReady回调中,MainActivity.java 先把地图定位到纽约曼哈顿(约 40.7679, -73.9818),缩放到 15 级,然后依次添加四个地标 Marker:

地标标题/摘要(来自 strings.xml)关联图片
联合国总部UN / United NationsUN_HQ.jpg
林肯中心Lincoln Center / Home of Jazz at Lincoln CenterAvery_Fisher_Hall.jpg
卡内基音乐厅Carnegie Hall / Where you go with practice, practice, practiceCarnegie_Hall.jpg
市中心俱乐部The Downtown Club / Original home of the Heisman Trophy无图片

每个 Marker 由addMarker()帮助方法创建(MainActivity.java):

private void addMarker(GoogleMap map, double lat, double lon, int title, int snippet, String image) { Marker marker= map.addMarker(new MarkerOptions().position(new LatLng(lat, lon)) .title(getString(title)) .snippet(getString(snippet))); if (image != null) { images.put(marker.getId(), Uri.parse("http://misc.commonsware.com/mapsv2/" + image)); } }

这里有一个贯穿全示例的关键设计:用marker.getId()作为 key,把 Marker 与其网络图片 URL 建立映射,存入HashMap<String, Uri> images。因为InfoWindowAdapter的每个回调都只携带Marker对象,适配器正是依靠marker.getId()从这张映射表里反查图片地址。图片统一存放在http://misc.commonsware.com/mapsv2/目录下,文件名与地标一一对应。

3.2 注册适配器与点击监听

map.setInfoWindowAdapter(new PopupAdapter(this, getLayoutInflater(), images)); map.setOnInfoWindowClickListener(this);
  • setInfoWindowAdapter()让地图引擎在每次弹出信息窗口时回调PopupAdapter;
  • setOnInfoWindowClickListener()让窗口本体可点击——示例中onInfoWindowClick只是用Toast弹出 Marker 标题(MainActivity.java),实战中可替换为跳转详情页等业务逻辑。

四、PopupAdapter 核心:异步图片加载的完整套路

getInfoContents的实现(PopupAdapter.java)是整份示例的技术核心,包含了三个值得反复品味的细节。

4.1 布局只 inflate 一次,视图复用

if (popup == null) { popup=inflater.inflate(R.layout.popup, null); }

信息窗口的弹出和关闭非常频繁,每次回调都重新inflate会带来不必要的对象创建开销。这里把popup.xml的布局实例缓存为成员变量,首次使用时才加载,后续直接复用同一个视图。

4.2 用 lastMarker 区分"切换 Marker"与"重复刷新"

if (lastMarker == null || !lastMarker.getId().equals(marker.getId())) { lastMarker=marker; // 更新标题、摘要,加载图片 ... }

只有切换到不同的 Marker 时才重新绑定文本、重新加载图片。同一个 Marker 的多次回调直接复用已填充好的视图,既省去重复的setText与图片请求,也避免了图片闪烁。

4.3 Picasso 链式加载与信息窗口刷新

Uri image=images.get(marker.getId()); ImageView icon=(ImageView)popup.findViewById(R.id.icon); if (image == null) { icon.setVisibility(View.GONE); // 无图 Marker 隐藏 ImageView } else { icon.setVisibility(View.VISIBLE); Picasso.with(ctxt).load(image).resize(iconWidth, iconHeight) .centerCrop().noFade() .placeholder(R.drawable.placeholder) .into(icon, new MarkerCallback(marker)); }

这条链式调用值得逐项拆解:

  • Picasso.with(ctxt):Picasso 2.x 的静态入口,返回单例实例;
  • .load(image):接受Uri,此处即images映射表里的网络地址;
  • .resize(iconWidth, iconHeight):把图片缩放到 dimens.xml 定义的 96×64dp,避免大图直接绘制到信息窗口;
  • .centerCrop():按目标尺寸等比裁剪居中,保证缩放后填满整个 ImageView 且不变形;
  • .noFade():禁用 Picasso 默认的淡入动画,让图片更快地"就位",减少信息窗口内容重绘时的违和感;
  • .placeholder(R.drawable.placeholder):加载期间先显示占位图(drawable-mdpi下的 placeholder.png,96×64,与本示例的 icon 尺寸一致),避免空白;
  • .into(icon, callback):加载完成/失败后回调第二个参数。

4.4 加载完成后强制刷新信息窗口

MarkerCallback(PopupAdapter.java)是整套方案中最容易踩坑、也最需要理解的一环:

static class MarkerCallback implements Callback { Marker marker=null; MarkerCallback(Marker marker) { this.marker=marker; } @Override public void onError() { Log.e(getClass().getSimpleName(), "Error loading thumbnail!"); } @Override public void onSuccess() { if (marker != null && marker.isInfoWindowShown()) { marker.showInfoWindow(); } } }

背景知识:Maps V2 的信息窗口不是真正的View,而是地图引擎把getInfoContents返回的视图**离线渲染成一帧位图(bitmap)**再贴到地图上。这意味着:

  1. 图片异步加载完成时,之前绘制到地图上的信息窗口快照不会自动更新;
  2. 因此必须在onSuccess回调里手动调用marker.showInfoWindow(),强制引擎重新走一遍getInfoContents,用已就绪的图片重新渲染窗口;
  3. 调用前用marker.isInfoWindowShown()判断窗口当前是否仍处于显示状态,避免"用户已关闭窗口却仍被强制重开"的异常行为;
  4. 图片加载失败(如网络异常)时,onError只记录日志,此时占位图或已缓存的旧图会保留在窗口中。

这个"异步回调 +showInfoWindow()重新渲染"的模式,是所有需要在信息窗口中展示异步内容(图片、动态文本等)的 Maps V2 应用的通用解法。

五、popup.xml:信息窗口的布局设计

自定义内容布局 popup.xml 采用水平LinearLayout:

<LinearLayout ... android:orientation="horizontal"> <ImageView android:id="@+id/icon" android:layout_width="@dimen/icon_width" android:layout_height="@dimen/icon_height" android:padding="2dip" android:src="@drawable/ic_launcher" android:contentDescription="@string/icon"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:orientation="vertical"> <TextView android:id="@+id/title" ... android:textSize="25sp" android:textStyle="bold"/> <TextView android:id="@+id/snippet" ... android:textSize="15sp"/> </LinearLayout> </LinearLayout>

要点:

  • 左侧ImageView(@+id/icon)承载异步加载的建筑照片,尺寸锁定为icon_width/icon_height(96×64dp),带 2dip 内边距;
  • android:src="@drawable/ic_launcher"作为 XML 层面的兜底占位,运行时 Picasso 会覆盖它;
  • 右侧垂直LinearLayout依次排列@+id/title(25sp 加粗)与@+id/snippet(15sp),与 PopupAdapter.java 中setText绑定的控件一一对应;
  • 由于无图的 Marker(如 The Downtown Club)需要隐藏图片,代码通过icon.setVisibility(View.GONE)处理,布局中无需额外分支。

六、AbstractMapActivity:Maps V2 的"上岗体检"

Maps V2 依赖 Google Play 服务与 OpenGL ES 2.0,二者缺一不可,因此所有 MapsV2 系列示例共用基类 AbstractMapActivity.java 做启动前的兼容性检查,MainActivity.onCreate中if (readyToGo())即以此为前提。

readyToGo()的检查逻辑分三层:

  1. Google Play 服务可用性:调用GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this),返回ConnectionResult.SUCCESS才继续;
  2. 错误可修复:若返回其他状态码,用checker.isUserResolvableError(status)判断用户能否通过更新商店应用等方式解决;能解决则弹出ErrorDialogFragment(内部调用checker.getErrorDialog()生成系统对话框),并在对话框关闭时finish()当前 Activity;
  3. 不可修复:直接Toast提示 "Google Maps V2 not available!" 并关闭页面。

除此之外,该类还复用了 Android CTS 测试中提取的getVersionFromPackageManager()方法(AbstractMapActivity.java),通过PackageManager.getSystemAvailableFeatures()找到name == null的 OpenGL ES 特性条目,读取reqGlEsVersion的高 16 位得到主版本号,要求必须 ≥ 2(即 OpenGL ES 2.0)。这条检查与 AndroidManifest.xml 中的声明相互印证:

<uses-feature android:glEsVersion="0x00020000" android:required="false"/>

android:required="false"表示应用不强制要求设备具备 OpenGL ES 2.0(这样设备仍可安装),真正的拦截由readyToGo()在运行时完成。

七、Manifest 与构建配置细节

7.1 清单文件要点

AndroidManifest.xml 除前面提到的 API Key 与uses-feature外,还有两处值得注意:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" />
  • WRITE_EXTERNAL_STORAGE加上maxSdkVersion="22":仅在 Android 6.0(API 23)之前的系统上申请写外部存储权限。这是为兼容旧版本 Picasso 等图片库把缓存写入外部存储的旧行为,Android 6.0+ 后图片库默认使用内部缓存目录,不再需要该权限;
  • 应用内还需声明com.google.android.gms.version的meta-data,其值为@integer/google_play_services_version,用于校验设备上 Google Play 服务版本是否与应用匹配。

7.2 构建方式

  • Gradle 构建:以 MapsV2/ImagePopups/build.gradle 为顶层脚本(AGP 3.1.4),配合 app/build.gradle 即可用 Android Studio 直接打开MapsV2/ImagePopups目录运行;
  • 旧式构建:project.properties 中通过android.library.reference.1引用本机 SDK 下的google-play-services_lib,属 Ant 时代的兼容残留,仅作参考。

八、可复用的实战要点总结

把本示例提炼成可直接套用的工程模式:

  1. 信息窗口定制:getInfoWindow返回null保留原生窗口外观,getInfoContents返回自定义布局,兼顾美观与自由排版;
  2. 图片定位:用marker.getId()作为 key 维护"Marker → 图片 URL"映射表,适配器回调时反查,解耦 Marker 与图片数据;
  3. 异步刷新:Picasso 等图片库的加载是异步的,而信息窗口是位图快照——务必在加载完成的回调中调用marker.showInfoWindow()并配合isInfoWindowShown()判断,否则图片永远不会显示;
  4. 视图复用:布局缓存 +lastMarker判断,减少重复 inflate 与重复网络请求;
  5. 尺寸控制:resize+centerCrop把任意尺寸的网络图片约束到固定尺寸(96×64dp),配合placeholder占位图避免加载期空白;
  6. 启动体检:复用 AbstractMapActivity 的readyToGo()逻辑,处理 Google Play 服务缺失与 OpenGL ES 版本不足的设备。

若想对比"纯文字信息窗口"与"自定义图片信息窗口"的差异,可同时阅读同目录下的 MapsV2/Popups 示例;ImagePopups 本身则提供了从文字到图文信息窗口升级的最短路径。需要说明的是,示例中的图片托管于http://misc.commonsware.com/mapsv2/,演示时依赖该站点可访问;在生产项目中,应将图片资源替换为自己的 CDN 或本地资源,并更换 AndroidManifest.xml 中的 Maps API Key。

  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

Source code to omnibus edition of _The Busy Coder's Guide to Android Development_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载

相关推荐

上一篇:K-NET硬件加速完全手册:SP670网卡TSO、LRO、硬件校验和实战指南
下一篇:3分钟上手openEuler/SBC-sig:从镜像下载到系统配置的快速入门指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询