- 示例工程
- 教程
【免费下载链接】cw-omnibus
Source code to omnibus edition of _The Busy Coder's Guide to Android Development_
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")。展开来说,它要解决三个层面的问题:
- 结构层:用哪种
PagerAdapter来管理多个 Fragment 页面; - 地图层:每个页面放什么地图组件,如何初始化和打标记;
- 交互层:地图本身会响应拖拽、捏合手势,如何不与
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); }它做了三层判断:
- Google Play services 是否可用:通过
GoogleApiAvailability.isGooglePlayServicesAvailable()检测; - OpenGL ES 版本是否达标:
getVersionFromPackageManager()遍历PackageManager.getSystemAvailableFeatures(),找出name == null的 feature(即 OpenGL ES 版本特性),解析其reqGlEsVersion的主版本号,要求 ≥ 2——这段实现来源于 Android CTS 的OpenGlEsVersionTest,源文件中有明确注释说明出处; - 错误是否可修复:若错误码是用户可解决的(如 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 所必需的,二者缺一不可。
九、运行与调试要点
- 替换 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; - 在真机或带 Google APIs 的模拟器上运行:地图依赖 Google Play services,普通模拟器镜像(无 GMS)会直接走
readyToGo()的失败分支,弹出不可用提示或退出; - 构建工具链:示例基于 Gradle 3.1.4 + AGP 3.1.4、compileSdk 27,若用更新的 Android Studio 打开,可考虑按需升级 Gradle 插件版本(本文只介绍当前仓库的构建方式,不展开迁移步骤);
- 观察行为:运行后顶部
PagerTabStrip会显示 "Map #1" 至 "Map #10" 十个标签,每页都是纽约市的一幅地图;在非地图区域横向滑动切换页面,在地图区域横向拖动则平移地图,可直观验证MapAwarePager.canScroll()的手势分流效果。
十、小结:可迁移到其他场景的通用模式
MapsV2/Pager虽然体量很小,却提炼出了一套可复用的组合模式:
| 关注点 | 采用的组件/方案 | 源码位置 |
|---|---|---|
| 多页地图托管 | FragmentStatePagerAdapter | MapPageAdapter.java |
| 单页地图 | SupportMapFragment+OnMapReadyCallback | PageMapFragment.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_
相关推荐
老电脑装 Windows 11 总是被拦?用 FlyOOBE 10 分钟搞定完整升级
老电脑装 Windows 11 总是被拦?用 FlyOOBE 10 分钟搞定完整升级 还在因为"这台电脑不满足 Windows 11 系统要求"的提示而卡在升级
示例工程教程Thorium 浏览器:老电脑跑起来不卡、视频网站能看、隐私默认关好的 Chromium 优化版 3 分钟上手指南
Thorium 浏览器:老电脑跑起来不卡、视频网站能看、隐私默认关好的 Chromium 优化版 3 分钟上手指南 Thorium 是一款基于 Chromium
桌面应用跨平台深入解析visgl/react-google-maps中的3D地图组件实现
深入解析visgl/react google maps中的3D地图组件实现 本文将详细介绍如何在visgl/react google maps项目中实现3D地球
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考