news 2026/10/16 1:48:18

在 ViewPager 中承载多张 Google Maps V2 地图:MapsV2/Pager 示例深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 ViewPager 中承载多张 Google Maps V2 地图:MapsV2/Pager 示例深度解析
  • 示例工程
  • 教程

【免费下载链接】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/Pager示例演示了一个非常实用的混合布局需求:在同一个ViewPager中横向滑动浏览多页 Google Maps V2 地图。本指南以该示例的官方 README 为骨架,结合其完整源码,讲解FragmentStatePagerAdapter+SupportMapFragment的组合方式、地图手势与ViewPager滑动手势的冲突处理,以及运行 Google Maps V2 应用所需的工程配置,帮助你快速在自己的项目中落地“多地图分页”能力。

一、示例要解决的核心问题

MapsV2/Pager的 README 只有一句话核心描述:该示例演示了在一个ViewPager中把多张地图作为页面("having several maps as pages in aViewPager")。展开来说,它要解决三个层面的问题:

  1. 结构层:用哪种PagerAdapter来管理多个 Fragment 页面;
  2. 地图层:每个页面放什么地图组件,如何初始化和打标记;
  3. 交互层:地图本身会响应拖拽、捏合手势,如何不与ViewPager的横向翻页手势互相“抢占”。

工程里 5 个 Java 文件正好一一对应这三个层面,下面逐个拆解。

二、工程结构总览

MapsV2/Pager是一个标准的 Android Gradle 单模块工程,源码集中在app/src/main下:

MapsV2/Pager/ ├── README.markdown # 示例说明(本篇主体) ├── settings.gradle # 声明 :app 模块 ├── build.gradle # 顶层构建脚本(Gradle 插件版本) ├── project.properties # 旧版 Ant 构建配置(保留) └── app/ ├── build.gradle # 模块依赖与 SDK 版本 └── src/main/ ├── AndroidManifest.xml # 权限、API Key、特性声明 ├── java/com/commonsware/android/mapsv2/pager/ │ ├── MainActivity.java # 入口 Activity │ ├── AbstractMapActivity.java # 地图可用性检查基类 │ ├── MapPageAdapter.java # FragmentStatePagerAdapter 子类 │ ├── PageMapFragment.java # 每页的地图 Fragment │ └── MapAwarePager.java # 自定义 ViewPager(手势协调) └── res/ ├── layout/activity_main.xml # 唯一布局:MapAwarePager ├── layout/legal.xml └── values/strings.xml # 标题、标记文案等

所有 Java 源文件头部都带有 Apache License 2.0 版权声明(Copyright (c) 2012–2017 CommonsWare, LLC),代码包名统一为com.commonsware.android.mapsv2.pager。

三、构建配置:依赖与 SDK 版本

示例使用 Gradle 构建。顶层 build.gradle 声明了 Android Gradle 插件3.1.4,仓库源为google()与jcenter()。

模块级 app/build.gradle 是核心,关键配置如下:

apply plugin: 'com.android.application' dependencies { implementation 'com.android.support:support-v4:27.1.1' implementation 'com.google.android.gms:play-services-maps:15.0.1' } android { compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 targetSdkVersion 27 } }

几点值得注意:

  • support-v4:27.1.1提供ViewPager、PagerTabStrip、FragmentStatePagerAdapter与FragmentActivity,是本示例全部分页能力的来源;
  • play-services-maps:15.0.1提供GoogleMap、SupportMapFragment、CameraUpdateFactory等 Google Maps V2 API;
  • minSdkVersion 14意味着该方案对 Android 4.0 起的设备都可用;
  • 顶层settings.gradle仅一句include ':app',模块结构非常简单。

仓库中同时还保留了 Eclipse/Ant 时代的 project.properties,其中target=android-19,并通过android.library.reference.1引用外部google-play-services_lib工程——这属于历史遗留,新开发直接使用上面的 Gradle 依赖即可。

四、Manifest 配置:API Key 与地图环境声明

AndroidManifest.xml 体现了运行 Google Maps V2 的三项必备声明:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> <uses-feature android:glEsVersion="0x00020000" android:required="false"/> <application ...> <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSyApXrquq0wzEvuLV3sXATJ27gmOKAx5cxI"/> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/> ... </application>

说明:

  • WRITE_EXTERNAL_STORAGE权限用于地图离线瓦片缓存(maxSdkVersion="22"是因为 Android 6.0 起存储权限模型改变,旧权限声明方式只对 API ≤ 22 生效);
  • glEsVersion="0x00020000"声明应用依赖 OpenGL ES 2.0,且required="false"表示该特性非硬性要求——如果设备不支持,应用仍可安装,只是地图功能不可用。这与基类AbstractMapActivity中的运行时检测逻辑相呼应(见下一节);
  • com.google.android.maps.v2.API_KEY元数据存放地图 API Key,示例中硬编码的是一个演示 Key,实际使用时必须替换为你自己在 Google Cloud Console 申请的 Key;
  • com.google.android.gms.version必须与工程所依赖的 Google Play services 版本一致,否则运行时会被拒绝。

五、入口 Activity 与地图可用性预检

5.1 MainActivity:极简的装配入口

MainActivity.java 只有几十行,逻辑非常清晰:

public class MainActivity extends AbstractMapActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (readyToGo()) { setContentView(R.layout.activity_main); ViewPager pager=findViewById(R.id.pager); pager.setAdapter(buildAdapter()); } } private PagerAdapter buildAdapter() { return(new MapPageAdapter(this, getSupportFragmentManager())); } }

关键点在于readyToGo()返回true才setContentView并装配ViewPager。如果设备上 Google Maps 不可用,界面根本不会创建,避免了 Fragment 加载到一半才报错的劣化体验。

5.2 AbstractMapActivity:三层可用性检查

AbstractMapActivity.java 是FragmentActivity的抽象子类,封装了所有 MapsV2 示例共用的“能不能跑地图”判定:

protected boolean readyToGo() { GoogleApiAvailability checker = GoogleApiAvailability.getInstance(); int status = checker.isGooglePlayServicesAvailable(this); if (status == ConnectionResult.SUCCESS) { if (getVersionFromPackageManager(this) >= 2) { return(true); } else { Toast.makeText(this, R.string.no_maps, Toast.LENGTH_LONG).show(); finish(); } } else if (checker.isUserResolvableError(status)) { ErrorDialogFragment.newInstance(status) .show(getFragmentManager(), TAG_ERROR_DIALOG_FRAGMENT); } else { Toast.makeText(this, R.string.no_maps, Toast.LENGTH_LONG).show(); finish(); } return(false); }

它做了三层判断:

  1. Google Play services 是否可用:通过GoogleApiAvailability.isGooglePlayServicesAvailable()检测;
  2. OpenGL ES 版本是否达标:getVersionFromPackageManager()遍历PackageManager.getSystemAvailableFeatures(),找出name == null的 feature(即 OpenGL ES 版本特性),解析其reqGlEsVersion的主版本号,要求 ≥ 2——这段实现来源于 Android CTS 的OpenGlEsVersionTest,源文件中有明确注释说明出处;
  3. 错误是否可修复:若错误码是用户可解决的(如 Play services 版本过旧需要升级),则弹出ErrorDialogFragment对话框;onDismiss中会finish()当前 Activity,防止用户停留在不可用的界面上。

也就是说,Manifest 里的glEsVersion声明是安装时的“软门槛”,readyToGo()是运行时的“硬校验”,两者配合保证了地图组件的可用前提。

六、适配器:用 FragmentStatePagerAdapter 承载 10 页地图

MapPageAdapter.java 继承自android.support.v4.app.FragmentStatePagerAdapter:

public class MapPageAdapter extends FragmentStatePagerAdapter { Context ctxt=null; public MapPageAdapter(Context ctxt, FragmentManager mgr) { super(mgr); this.ctxt=ctxt; } @Override public int getCount() { return(10); } @Override public Fragment getItem(int position) { return(new PageMapFragment()); } @Override public String getPageTitle(int position) { return(ctxt.getString(R.string.map_page_title) + String.valueOf(position + 1)); } }

三个 override 的含义:

  • getCount()返回 10:示例固定渲染 10 页地图,全部由同一个PageMapFragment实例化而来;
  • getItem()每次new PageMapFragment():每页一个独立的SupportMapFragment,拥有自己的地图实例与摄像机状态;
  • getPageTitle()拼接 "Map #1"…"Map #10":配合布局中的PagerTabStrip在顶部显示页码标签(字符串资源map_page_title定义为"Map #",见 strings.xml)。

为什么选FragmentStatePagerAdapter而不是FragmentPagerAdapter?关键差异在于:FragmentStatePagerAdapter会销毁不可见页面的 Fragment 实例与视图状态,只保留savedInstanceState,因此更适合“页面数量可能很多、且每页内容较重(地图正是典型的重型视图)”的场景,内存占用可控。翻页回来后,Fragment 会基于保存的状态重建,地图会恢复到退出屏幕前的摄像机位置——这也解释了下一节needsInit标志位存在的意义。

七、页面地图:SupportMapFragment + 一次性初始化

PageMapFragment.java 直接继承SupportMapFragment并实现OnMapReadyCallback:

public class PageMapFragment extends SupportMapFragment implements OnMapReadyCallback { private boolean needsInit=false; @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState == null) { needsInit=true; } getMapAsync(this); } @Override public void onMapReady(final GoogleMap map) { if (needsInit) { CameraUpdate center = CameraUpdateFactory.newLatLng(new LatLng(40.76793169992044, -73.98180484771729)); CameraUpdate zoom = CameraUpdateFactory.zoomTo(15); map.moveCamera(center); map.animateCamera(zoom); } addMarker(map, 40.748963847316034, -73.96807193756104, R.string.un, R.string.united_nations); addMarker(map, 40.76866299974387, -73.98268461227417, R.string.lincoln_center, R.string.lincoln_center_snippet); addMarker(map, 40.765136435316755, -73.97989511489868, R.string.carnegie_hall, R.string.practice_x3); addMarker(map, 40.70686417491799, -74.01572942733765, R.string.downtown_club, R.string.heisman_trophy); } private void addMarker(GoogleMap map, double lat, double lon, int title, int snippet) { map.addMarker(new MarkerOptions().position(new LatLng(lat, lon)) .title(getString(title)) .snippet(getString(snippet))); } }

设计上的两个细节值得借鉴:

1.needsInit区分“首次创建”与“重建恢复”。当savedInstanceState == null说明这是新创建的页面,需要把摄像机移动到初始位置(纽约市中心坐标,缩放到 15 级);反之,当FragmentStatePagerAdapter从保存的状态重建页面时,地图应恢复到用户上次看到的位置,不再强行重置摄像机。onMapReady是异步回调,而needsInit是在onViewCreated里同步判定的,时序上刚好覆盖。

2. 地图初始化全部走异步getMapAsync()。自 Google Maps Android API 早期版本起,获取GoogleMap对象就必须通过OnMapReadyCallback,避免在 MapView 尚未就绪时操作底层地图。示例中每个页面都在自己的 Fragment 生命周期内独立执行getMapAsync,互不干扰。

3. 标记内容本地化。四个地标(联合国总部、林肯中心、卡内基音乐厅、Downtown Club)的标题与副标题都来自 strings.xml 资源,addMarker()只负责把MarkerOptions装配出来。

八、MapAwarePager:解决地图与翻页的手势冲突

这是整个示例最有技术含量的一处。MapAwarePager.java 继承了ViewPager并只重写一个方法:

public class MapAwarePager extends ViewPager { public MapAwarePager(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof SurfaceView || v instanceof PagerTabStrip) { return(true); } return(super.canScroll(v, checkV, dx, x, y)); } }

8.1 冲突从何而来

ViewPager在判断一次横向拖动是“翻页”还是“交给子视图消费”时,会调用canScroll()检查目标子视图是否已经处理了水平滚动。问题是:

  • Google 地图底层是SurfaceView(MapView/SupportMapFragment 的渲染承载者)。SurfaceView不是常规的 View 树成员,很多设备上ViewPager默认的canScroll判定对SurfaceView不生效,结果是在地图上横向拖动时无法平移地图,反而直接触发翻页;
  • PagerTabStrip在canScroll检查中若不特殊处理,其所在区域的横向滑动也会被错误地交给上层,导致指示条区域无法正常响应。

8.2 解法:让 ViewPager 对地图“放手”

重写后的canScroll()明确:当目标视图是SurfaceView(即地图)或PagerTabStrip时,直接返回true,表示“这个视图自己会处理滚动,ViewPager 不应拦截”。于是:

  • 手指在地图区域横向拖动 → 地图平移(SurfaceView消费事件);
  • 手指在 Tab 条或非地图区域横向快速滑动 →ViewPager翻页;
  • 只有在ViewPager真正确认“该视图不处理”时,才走默认逻辑。

这就是“多地图分页”体验顺畅的关键所在,也是本示例命名为MapAwarePager(“懂地图的 Pager”)的原因。

8.3 布局中的用法

activity_main.xml 直接把自定义控件作为根节点,并在其内部声明了PagerTabStrip:

<com.commonsware.android.mapsv2.pager.MapAwarePager xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.view.PagerTabStrip android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top"/> </com.commonsware.android.mapsv2.pager.MapAwarePager>

注意:PagerTabStrip是ViewPager的“子标签”,不是独立的布局元素,必须像这样内嵌在ViewPager(此处即MapAwarePager)的标签内。MainActivity中findViewById(R.id.pager)找到的正是这个自定义的MapAwarePager。

MapAwarePager的构造函数签名(Context, AttributeSet)是自定义 View 被 XML inflate 所必需的,二者缺一不可。

九、运行与调试要点

  1. 替换 API Key:编辑 AndroidManifest.xml 中com.google.android.maps.v2.API_KEY的android:value,换成自己的 Key,并在 Google Cloud Console 为对应包名com.commonsware.android.mapsv2.pager启用 Maps SDK for Android;
  2. 在真机或带 Google APIs 的模拟器上运行:地图依赖 Google Play services,普通模拟器镜像(无 GMS)会直接走readyToGo()的失败分支,弹出不可用提示或退出;
  3. 构建工具链:示例基于 Gradle 3.1.4 + AGP 3.1.4、compileSdk 27,若用更新的 Android Studio 打开,可考虑按需升级 Gradle 插件版本(本文只介绍当前仓库的构建方式,不展开迁移步骤);
  4. 观察行为:运行后顶部PagerTabStrip会显示 "Map #1" 至 "Map #10" 十个标签,每页都是纽约市的一幅地图;在非地图区域横向滑动切换页面,在地图区域横向拖动则平移地图,可直观验证MapAwarePager.canScroll()的手势分流效果。

十、小结:可迁移到其他场景的通用模式

MapsV2/Pager虽然体量很小,却提炼出了一套可复用的组合模式:

关注点采用的组件/方案源码位置
多页地图托管FragmentStatePagerAdapterMapPageAdapter.java
单页地图SupportMapFragment+OnMapReadyCallbackPageMapFragment.java
手势协调自定义ViewPager覆写canScroll()MapAwarePager.java
可用性预检GoogleApiAvailability+ OpenGL ES 2 检测AbstractMapActivity.java

这套模式不仅适用于“地图分页”,当你在ViewPager(或横向滑动的容器)内放入任何基于SurfaceView/TextureView的重型渲染视图(视频播放器、自绘游戏画布、相机预览等)时,都可以复用MapAwarePager的思路:覆写canScroll(),识别并放行那些“自己会滚动”的子视图,从而让嵌套手势各归其位。示例源码所在的MapsV2/目录下还有 Basic、Markers、Camera 等 20 个姊妹示例,若需要进一步了解地图基础用法,可以对照阅读。

  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

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

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载
上一篇:AMD Ryzen终极性能解锁:SMUDebugTool完全掌控指南
下一篇:终极指南:如何使用免费AMD Ryzen调试工具SMUDebugTool解锁处理器潜能

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 1:45:18

ResNet-152农业定制版:植物病害识别流水线实战

简介&#xff1a;本资源是基于ResNet152深度卷积神经网络构建的植物病害智能识别模型&#xff0c;面向人工智能初学者、计算机视觉实践者及农业智能化应用开发者&#xff0c;解决植物叶片图像细粒度分类&#xff08;共38类病害&#xff09;这一典型迁移学习任务。压缩包含2000个…

作者头像 李华
网站建设 2026/10/16 1:44:43

Netty线程模型与核心组件实战:从EventLoop到Handler避坑指南

Netty 这套东西我用了差不多两年多&#xff0c;期间从零搭过推送服务、网关、私有协议解析&#xff0c;也在生产环境里排查过内存泄漏和线程阻塞的问题。回过头看&#xff0c;最值得掰开揉碎讲清楚的&#xff0c;还是它的线程模型和那几个核心组件。倒不是因为面试爱问&#xf…

作者头像 李华
网站建设 2026/10/16 1:43:41

软考 系统架构设计师历年真题集萃(116)

接前一篇文章:软考 系统架构设计师系列知识点之杂项集萃(115) 第213题 ANSI/IEEE 1471-2000是对软件密集型系统的架构进行描述的标准。在该标准中,( )这一概念主要用于描述架构模型。在此基础上,通常采用( )描述某个利益相关人(Stakeholder)所关注架构模型的某一方…

作者头像 李华