这篇文章是基于2023年7月在Google开发人员组延长的Google I/O的演讲。
很难想象JetPack构成1.0在July 2021释放。快进了两年,与24% of the top 1000 apps on Google Play adopting Compose相关,很容易理解原因。 在所有兴奋中,现代Android开发的一个角落,我觉得很少关注的是Google Maps。自从我使用SDK以来已经有一段时间了,因此看到Google Maps赶上了时代并发布了自己的Compose library。
这将是在地图领域工作的公司和工程师的欢迎新闻。移动制图是一个$ 35.5B的行业,预测到2028年将其筹集到87.7B美元。复合年增长率(CAGR)为19.83%。 Source
为什么这很重要?更大的市场意味着公司从移动映射的应用中获得收入的更多机会。这些范围从通常的用例,食物,杂货交付和乘车服务。但是,如果您深入研究,有些应用程序会立即显而易见。以下是我短暂搜索后可以找到的示例。
移动地图非常适合智能城市,有助于管理城市的心跳,并以更好地理解和应对其挑战的方式可视化数据。对城市规划人员,应急组织或日常居民有用。
资源管理还受益于映射解决方案。从农业到钓鱼,采矿再到林业,地图为这一工作中的地图提供了以可持续方式收获材料的正确决定的观点。
运输在很大程度上依赖于映射技术。不仅是Google Maps或Uber之类的消费者应用程序,而且业务级别的功能如了解企业车队所在的企业。运输机构还使用地图来管理流量并帮助决定在哪里引导流量以减轻流量。
最后,随着气候变化和天气越来越不可预测,地图允许气象机构,紧急响应单位和野生动植物保护主义者了解我们的世界如何变化,以及我们可以采取积极步骤来减少这一点。
Sources: Mordor Intelligence, GMInsights, Allied Market Research, EMR Research, Google Earth Outreach, Research & Markets
随着世界提供越来越多的数据,现在是学习如何将这些数据放在地图上的好时机。让我们这样做并回到代码。
使用Google地图
撰写的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"
}
Google Maps for Compose建立在Google Maps SDK之上,因此您需要导入撰写库和MAPS SDK。您是否需要在Google Maps SDK中使用大多数对象,因为组成的库包装在Composables中。
。utils和小部件库是可选的依赖性。 UTILS库提供了在地图上群集标记的能力,而小部件则提供了其他UI组件。您将在以后看到这些。
在这篇文章中,我包括伴奏者的请求权限库,以演示如何请求位置权限,这是经常使用地图的许可。伴奏者是一个实验库,供Google尝试并收集尚未撰写的功能的反馈。
最后,您需要转到Google Developer Console,注册Google Maps SDK API密钥,然后将其添加到您的项目中。 Google Maps Developer Docs的指南关于如何执行此操作。
安全提示:在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
结合使用它来设置相机的初始位置。当您使用双手或编程方式移动时,此方法会记住地图的位置。没有这种方法,可以将地图重新添加回到其在每个状态上的初始位置。
接下来,创建一个GoogleMap
组成并传递您选择和相机状态的修饰符。 GoogleMap
还提供了一个插槽API来传递其他组合,这些组合是您要在地图上绘制的内容。
添加一个Marker
合并,然后添加一个包含标记内部位置的MarkerState
。最后,添加标记的标题和描述。
运行此功能可在海德公园(Hyde Park)带有标记的西伦敦(West London)。
自定义标记窗口
您可以使用MarkerInfoWindowContent
组合来自定义标记的窗口。这也具有基于插槽的API,这意味着您可以通过自己的组成件传递以在窗口中渲染自定义UI。
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 Maps和您的设备会感谢您的Ether,因为它必须呈现每个标记,从而影响性能和电池寿命。
解决方案是聚类,一种将彼此接近的技术分组为单个标记。这种聚类在缩放级别上进行。当您缩放地图时,标记将将群集组合在一起,因为您将群集放大将分为单个标记。
用于撰写的Google Maps通过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
接口。该界面为每个标记的位置,标题和片段提供了群集。
放大进出,您会看到行动中的聚类。
获得位置许可
地图和用户位置经常齐头并进,因此某些映射应用程序要求用户位置征求许可是有意义的。
尊重用户许可如果您这样做,则位置权限是从用户那里获得的最敏感的权限之一。确保您通知用户为什么需要此许可并积极显示授予它的好处。如果您的应用程序部分运行而无需许可,则奖励积分。
Google提供了有关如何处理users location的一些很好的指南,以及accessing location data in the background的单独指南。
因此,您可以进行尽职调查,并决定您确实需要用户许可才能访问位置。在伴奏者中的权限库中,您会这样做:
// 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")
}
}
}
通过伴奏者,我们检查一下该应用程序是否可以访问ACCESS_FINE_LOCATION
许可,还是英语中高水平的GPS精度。重要的是要在Android清单中包括所请求的权限,因为您将不得不要求权限。 Android System和Google Play商店还使用清单来了解您的应用程序的工作原理并告知用户。
如果未授予许可,则显示一个小型对话框,以说明对通过系统启动权限请求的许可和按钮的需求。
动画地图
虽然大多数地图应用程序都要求用户通过触摸移动地图,但撰写的Google Maps提供了以编程方式移动地图的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秒钟。
显示街道视图
这不仅是播放的谷歌地图可帮助您。您还可以使应用程序访问街道视图,显示一个位置的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提供了许多可以做到这一点的组合,在这篇文章中,我们将使用Circle
Composoble。
如果您的应用程序使用地理方案对用户位置的更改做出反应,则可以使用圆圈。一个圆可以代表地理林在内的区域。
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已覆盖您。
使用小部件库,您可以访问DisappearingScaleBar
和ScaleBar
Composables。这些是位于地图顶部的UI组件,为用户提供了根据Zoom级别变化的距离。
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
)
}
添加组合后,您将获得一个根据地图顶部的缩放级别更改的比例栏。
帮助和支持
Google Maps for Compose是使用Google Maps合作的好方法,还有很多还可以学习的方法。如果您需要帮助,我建议以下几个地方:
Google Maps for Compose Repo:包含库源代码的回购。包含有关如何使用库以及您可以在哪里提交错误报告和贡献的代码示例
Google Maps for Android Website:去学习Android的Google Maps背后的概念的地方。这些是高水平的,不使用撰写库,但是重要的是要知道这些图书馆。
Google Maps Platform Discord Google Maps的官方Discord服务器。在这里,您可以找到人们讨论多个平台的Google地图,询问和提供帮助,并展示自己的作品。