Этот пост основан на выступлении, прозвучавшем на конференции Google I/O Extended для Google Developers Group в Лондоне в июле 2023 года. Слайды доклада доступны здесь.
Трудно представить, что Jetpack Compose 1.0 выйдет в июле 2021 года . Перенесемся на два года вперед: 24% из 1000 лучших приложений в Google Play используют Compose, и легко понять, почему.
Среди всего интересного, одному аспекту современной разработки Android, который, как мне кажется, уделяется мало внимания, являются Google Maps. Прошло много времени с тех пор, как я использовал SDK, поэтому был приятно удивлен, увидев, что Google Maps идет в ногу со временем и выпустил собственную библиотеку Compose .
Это будет приятной новостью для компаний и инженеров, работающих в области картографирования. Мобильное картографирование — это индустрия с оборотом в 35,5 миллиарда долларов, по прогнозам, к 2028 году она вырастет до 87,7 миллиарда долларов при совокупном годовом темпе роста (CAGR) 19,83%. Источник
Почему это важно? Больший рынок означает больше возможностей для компаний получать доход от приложений мобильного картографирования. Они варьируются от обычных вариантов использования до доставки еды, продуктов и такси. Однако если копнуть глубже, есть приложения, которые не сразу очевидны.
Ниже приведены примеры, которые я смог найти после непродолжительного поиска:
Мобильные карты отлично подходят для умных городов, помогая управлять пульсом города и визуализировать данные, чтобы лучше понимать и реагировать на его проблемы. Полезно для градостроителей, организаций по реагированию на чрезвычайные ситуации или обычных жителей.
Управление ресурсами также выигрывает от картографических решений. От сельского хозяйства до рыболовства, от горнодобывающей промышленности до лесного хозяйства — карты дают тем, кто занимается этой сферой деятельности, возможность принимать правильные решения по устойчивому сбору материалов.
Транспорт во многом зависит от картографических технологий. Не только потребительские приложения, такие как Google Maps или Uber, но и функции бизнес-уровня, такие как понимание того, где находится парк транспортных средств компании. Транспортные агентства также используют карты для управления дорожным движением и помогают принимать решения о том, куда направить движение, чтобы облегчить поток.
Наконец, поскольку изменение климата и погода становятся все более непредсказуемыми, карты позволяют метеорологическим агентствам, подразделениям реагирования на чрезвычайные ситуации и защитникам дикой природы понять, как меняется наш мир и что мы можем сделать, чтобы предпринять позитивные шаги для уменьшения этого явления.
Источники:Mordor Intelligence , GMInsights , Allied Market Research , EMR Research , Google Earth Outreach , Research & Markets.
Поскольку мир предоставляет все больше и больше данных, самое время научиться наносить эти данные на карту. Давайте сделаем это и вернемся к коду.
Карты Google для Compose используют следующие зависимости:
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 для Compose созданы на основе Google Maps SDK, поэтому вам необходимо импортировать библиотеку Compose и Maps SDK. Вам не понадобится использовать большинство объектов в Google Maps SDK, поскольку библиотека Compose упаковывает большинство из них в Composables.
Библиотеки утилит и виджетов являются необязательной зависимостью. Библиотека utils предоставляет возможность группировать маркеры на картах, а виджеты предоставляют дополнительные компоненты пользовательского интерфейса. Вы увидите, как они используются позже.
В этот пост я включил библиотеку разрешений запросов от Аккомпаниатора, чтобы продемонстрировать, как запрашивать разрешения на определение местоположения — часто используемое разрешение с картами. Аккомпаниатор — это экспериментальная библиотека Google, которую можно опробовать и собрать отзывы о функциях, еще не включенных в Jetpack Compose.
Наконец, вам нужно перейти в консоль разработчика Google , зарегистрировать ключ API Google Maps SDK и добавить его в свой проект. В Документах разработчика 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
также предоставляет 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 для 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
. Интерфейс предоставляет кластеру позицию, заголовок и фрагмент для каждого маркера.
Увеличьте и уменьшите масштаб, и вы увидите кластеризацию в действии.
Карты и положение пользователя часто идут рука об руку, поэтому некоторым картографическим приложениям имеет смысл запрашивать разрешение на определение местоположения пользователя.
Если вы это сделаете, относитесь к разрешению пользователя с уважением : разрешение на определение местоположения является одним из наиболее важных разрешений, которые необходимо получить от пользователя.
Обязательно сообщите пользователю, почему вам нужно это разрешение, и активно продемонстрируйте преимущества его предоставления. Бонусные баллы, если ваше приложение частично функционирует без необходимости получения разрешения.
Google предоставляет несколько отличных руководств о том, как обрабатывать местоположение пользователей , а также отдельное руководство по доступу к данным о местоположении в фоновом режиме .
Итак, вы провели должную осмотрительность и решили, что вам действительно нужно разрешение пользователя для доступа к местоположению. С библиотекой разрешений в Аккомпаниаторе вы делаете это следующим образом:
// 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 и магазин 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
при каждом касании маркера. Если выбран маркер, мы отображаем Street View, передавая позицию маркера.
Возможно, вы захотите нарисовать на карте свои собственные фигуры и аннотации. 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 поможет вам.
Используя библиотеку виджетов, вы получаете доступ к составным объектам DisappearingScaleBar
и ScaleBar
. Это компоненты пользовательского интерфейса, расположенные в верхней части карты и предоставляющие пользователям информацию о расстоянии, которая меняется в зависимости от уровня масштабирования.
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 для Compose — отличный способ работы с Картами Google, и здесь можно еще многому научиться. Вот несколько мест, которые я рекомендую, если вам нужна помощь: