很难想象 Jetpack Compose 1.0 会在2021 年 7 月发布。快进两年, Google Play 上排名前 1000 的应用中有 24% 采用了 Compose,这很容易理解为什么。
在所有这些令人兴奋的事情中,我觉得现代 Android 开发中一个很少受到关注的领域是 Google 地图。我已经有一段时间没有使用过 SDK 了,因此我很高兴地看到 Google 地图正在与时俱进并发布了自己的Compose 库。
对于从事地图绘制行业的公司和工程师来说,这将是一个好消息。移动地图绘制行业价值 355 亿美元,预计到 2028 年将增长至 877 亿美元,复合年增长率 (CAGR) 为 19.83%。 来源
为什么这很重要?市场越大,公司从移动地图应用中获得收入的机会就越多。这些应用包括常见的用例、食品、杂货配送和叫车服务。然而,如果你深入挖掘,就会发现有些应用并不那么明显。
以下是我经过简单搜索后找到的示例:
移动地图非常适合智慧城市,有助于管理城市脉搏,并以可视化的方式呈现数据,以便更好地了解和应对城市挑战。对于城市规划者、应急响应组织或普通居民来说都很有用。
资源管理也受益于地图解决方案。从农业到渔业、从采矿业到林业,地图为从事这些行业的人们提供了正确的视角,使他们能够做出正确的决策,以可持续的方式收获资源。
交通运输严重依赖地图技术。不仅仅是像谷歌地图或优步这样的消费者应用,还有企业级功能,比如了解企业车队的位置。交通机构还使用地图来管理交通,并帮助决定将交通引导到哪里以缓解交通流量。
最后,随着气候变化和天气变得越来越难以预测,地图可以让气象机构、应急响应单位和野生动物保护主义者了解我们的世界是如何变化的,以及我们可以采取哪些积极措施来减少这种变化。
资料来源:Mordor Intelligence 、 GMInsights 、 Allied Market Research 、 EMR Research 、 Google Earth Outreach 、 Research & Markets
随着世界提供的数据越来越多,现在正是学习如何将这些数据放到地图上的好时机。让我们开始做这件事,然后回到代码。
Compose 版 Google Maps 依赖于以下依赖项:
dependencies { implementation "com.google.maps.android:maps-compose:2.11.4" implementation "com.google.android.gms:play-services-maps:18.1.0" // Optional Util Library implementation "com.google.maps.android:maps-compose-utils:2.11.4" implementation 'com.google.maps.android:maps-compose-widgets:2.11.4' // Optional Accompanist permissions to request permissions in compose implementation "com.google.accompanist:accompanist-permissions:0.31.5-beta" }
Compose 版 Google Maps 是基于 Google Maps SDK 构建的,因此您需要导入 Compose 库和 Maps SDK。您不需要使用 Google Maps SDK 中的大多数对象,因为 Compose 库将其中大多数对象封装在 Composable 中。
Utils 和 widgets 库是可选依赖项。Utils 库提供了在地图上聚集标记的功能,而 widgets 则提供了额外的 UI 组件。稍后您将看到它们的使用情况。
在本文中,我添加了来自 Accompanist 的请求权限库,以演示如何请求位置权限,这是地图中经常使用的权限。Accompanist 是一个实验性库,供 Google 试用并收集尚未包含在 Jetpack Compose 中的功能的反馈。
最后,您需要转到Google 开发者控制台,注册 Google Maps SDK API 密钥,并将其添加到您的项目中。Google Maps 开发者文档中有一个指南,介绍如何执行此操作。
安全提示:在 Google 开发者控制台中锁定您的 API 密钥,使其仅适用于您的应用程序。这可避免任何未经授权的使用。
显示地图非常简单,如下所示:
setContent { val hydePark = LatLng(51.508610, -0.163611) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { Marker( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) } }
创建一个具有区域位置的LatLng
对象,并将其与rememberCameraPositionState
结合使用,以设置相机的初始位置。此方法会在您用手或以编程方式移动时记住地图的位置。如果没有此方法,Compose 会在每次状态更改时将地图重新计算回其初始位置。
接下来,创建一个GoogleMap
组合并传入您选择的修饰符和相机状态。GoogleMap GoogleMap
提供了一个 Slot API 来传入其他可组合项,这些可组合项就是您想要在地图上绘制的内容。
添加一个Marker
可组合项,然后添加一个包含标记内部位置的MarkerState
。最后,添加标记的标题和说明。
沿着这条路走下去,可以看到西伦敦美丽的鸟瞰图,海德公园里有一个标记。
您可以使用MarkerInfoWindowContent
可组合项自定义标记的窗口。它还具有基于插槽的 API,这意味着您可以传入可组合项以在窗口中呈现自定义界面。
setContent { val hydePark = LatLng(51.508610, -0.163611) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { MarkerInfoWindowContent( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) { marker -> Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( modifier = Modifier.padding(top = 6.dp), text = marker.title ?: "", fontWeight = FontWeight.Bold ) Text("Hyde Park is a Grade I-listed parked in Westminster") Image( modifier = Modifier .padding(top = 6.dp) .border( BorderStroke(3.dp, color = Color.Gray), shape = RectangleShape ), painter = painterResource(id = R.drawable.hyde_park), contentDescription = "A picture of hyde park" ) } } } }
运行此程序,当您点击标记时,标记上方会显示自定义窗口。
显示多个标记非常简单,只需添加所需的数量即可。让我们为伦敦西部的几个不同公园添加标记。
setContent { val hydePark = LatLng(51.508610, -0.163611) val regentsPark = LatLng(51.531143, -0.159893) val primroseHill = LatLng(51.539556, -0.16076088) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { // Marker 1 Marker( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) // Marker 2 Marker( state = MarkerState(position = regentsPark), title = "Regents Park", snippet = "Marker in Regents Park" ) // Marker 3 Marker( state = MarkerState(position = primroseHill), title = "Primrose Hill", snippet = "Marker in Primrose Hill" ) } }
运行代码,您将看到您的标记出现在地图上。
地图在短时间内可能会变得很繁忙。如果您尝试显示 300 个标记,用户将很难从视觉上理解正在发生的事情。Google 地图和您的设备也不会感谢您,因为它必须渲染每个标记,从而影响性能和电池寿命。
解决方案是聚类,这是一种将彼此靠近的标记分组为单个标记的技术。此聚类以缩放级别为基础。当您缩小地图时,标记将分组为一个聚类,当您放大地图时,聚类将拆分为单个标记。
Google Maps for Compose 通过Clustering
可组合项提供开箱即用的功能。无需编写复杂的排序或过滤即可进行聚类。
setContent { val hydePark = LatLng(51.508610, -0.163611) val regentsPark = LatLng(51.531143, -0.159893) val primroseHill = LatLng(51.539556, -0.16076088) val crystalPalacePark = LatLng(51.42153, -0.05749) val greenwichPark = LatLng(51.476688, 0.000130) val lloydPark = LatLng(51.364188, -0.080703) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { val parkMarkers = remember { mutableStateListOf( ParkItem(hydepark, "Hyde Park", "Marker in hyde Park"), ParkItem(regentspark, "Regents Park", "Marker in Regents Park"), ParkItem(primroseHill, "Primrose Hill", "Marker in Primrose Hill"), ParkItem(crystalPalacePark, "Crystal Palace", "Marker in Crystal Palace"), ParkItem(greenwichPark, "Greenwich Park", "Marker in Greenwich Park"), ParkItem(lloydPark, "Lloyd park", "Marker in Lloyd Park"), ) } Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is tapped }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is tapped }) } } data class ParkItem( val itemPosition: LatLng, val itemTitle: String, val itemSnippet: String) : ClusterItem { override fun getPosition(): LatLng = itemPosition override fun getTitle(): String = itemTitle override fun getSnippet(): String = itemSnippet }
请注意添加了ParkItem
数据类。我们需要它,因为传入Clustering
可组合项的项目必须符合ClusterItem
接口。该接口为 Cluster 提供每个标记的位置、标题和片段。
放大和缩小,您将看到聚类的实际操作。
地图和用户位置通常密切相关,因此某些地图应用程序请求用户位置权限是有意义的。
如果您这样做,请尊重用户的权限,位置权限是从用户那里获取的最敏感的权限之一。
确保告知用户为何需要此权限,并积极展示授予该权限的好处。如果您的应用部分功能完全不需要权限,则可获得加分。
谷歌提供了一些关于如何处理用户位置的精彩指南,以及关于在后台访问位置数据的单独指南。
因此,您已尽职尽责,并决定确实需要用户的许可才能访问该位置。使用 Accompanist 中的权限库,您可以这样做:
// Don't forget to add the permissions to AndroidManifest.xml val allLocationPermissionState = rememberMultiplePermissionsState( listOf(android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION) ) // Check if we have location permissions if (!allLocationPermissionsState.allPermissionsGranted) { // Show a component to request permission from the user Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier .padding(horizontal = 36.dp) .clip(RoundedCornerShape(16.dp)) .background(Color.white) ) { Text( modifier = Modifier.padding(top = 6.dp), textAlign = TextAlign.Center, text = "This app functions 150% times better with percise location enabled" ) Button(modifier = Modifier.padding(top = 12.dp), onClick = { allLocationPermissionsState.launchMultiplePermissionsRequest() }) { Text(text = "Grant Permission") } } }
通过 accompanist,我们正在检查应用是否有权访问ACCESS_FINE_LOCATION
权限,或英语中高水平的 GPS 精度。在 Android 清单中包含所请求的权限非常重要,否则您将无法请求这些权限。
Android 系统和 Google Play 商店也使用清单来了解您的应用如何运作并告知用户。
如果未授予权限,则会显示一个小对话框,解释权限需求,并显示一个通过系统启动权限请求的按钮。
虽然大多数地图应用都要求用户通过触摸来移动地图,但 Google Maps for Compose 提供了以编程方式移动地图的 API。如果您想响应事件导航到特定区域,这会很有用。
在此示例中,我们将通过标记集合轻轻地导航应用程序。
Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the click is tapped false }, onClusterItemClick = { marker -> // Handle when the marker is tapped }) LaunchedEffect(key1 = "Animation") { for (marker in parkMarkers) { cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( marker.itemPosition, // LatLng 16.0f), // Zoom level 2000 // Animation duration in millis ), delay(4000L) // Delay in millis } } } }
这里的关键部分是LaunchedEffect
中的代码。对于每个标记,应用都会设置一个cameraPositionState.animate()
调用来导航到标记。相机通过使用CameraUpdateFactory.newLatLngZoom()
创建的相机更新来接收此信息。
此方法采用LatLng
、表示地图缩放级别的浮点数以及用于设置动画持续时间的长整型值。
最后,为了间隔动画,我们使用delay()
在每个动画之间添加 4 秒的暂停。
Google Maps for Compose 不仅能为您提供航拍地图。您还可以授予应用访问街景的权限,以显示某个位置的 360 度视图。您可以使用StreetView
可组合项来实现此目的:
var selectedMarker: ParkItem? by remember { mutableStateOf(null) } if (selectedMarker != null) { StreetView(Modifier.fillMaxSize(), streetViewPanoramaOptionsFactory = { StreetViewPanoramaOptions().position(selectedMarker!!.position) }) } else { Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } }
在此示例中,每当点击标记时,我们都会设置selectedMarker
变量。如果选择了标记,我们会显示街景,并传递标记的位置。
您可能希望在地图上绘制自己的形状和注释。Google Maps for Compose 提供了许多可组合项来执行此操作,在本文中我们将使用Circle
可组合项。
如果您的应用使用地理围栏来响应用户位置的变化,则圆形是不错的选择。圆形可以表示地理围栏处于活动状态的区域。
Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } parkMarkers.forEach { Circle( center = it.position, radius = 120.0, fillColor = Color.Green, strokeColor = Color.Green ) }
在这里,我们为每个标记设置一个圆圈。创建圆圈需要向其传递一个位置和圆圈半径的大小。我们还使用两个可选参数来设置边框的颜色并填充圆圈的颜色。
一张好的地图通常带有图例和图表,显示地图上某一空间单位的测量值相当于多少距离。这可以让您了解地图中涉及的空间,因为并非每张地图都使用相同的测量形式。
对于可以放大和缩小的数字地图,这增加了一层特殊的复杂性,因为所表示的距离可以动态变化。幸运的是,Google Maps for Compose 可以满足您的需求。
使用 Widgets 库,您可以访问DisappearingScaleBar
和ScaleBar
可组合项。这些是位于地图顶部的 UI 组件,为用户提供根据缩放级别而变化的距离测量值。
Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { // You can also use ScaleBar DisappearingScaleBar( modifier = Modifier .padding(top = 5.dp, end = 15.dp) .align(Alignment.TopStart), cameraPositionState = cameraPositionState ) Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } parkMarkers.forEach { Circle( center = it.position, radius = 120.0, fillColor = Color.Green, strokeColor = Color.Green ) }
添加可组合项后,您将获得一个根据地图顶部的缩放级别而变化的 ScaleBar。
Google Maps for Compose 是使用 Google Maps 的绝佳方式,还有很多东西需要学习。如果您需要帮助,我推荐以下几个地方: