Геолокация¶
MapKit отдаёт три вида источника местоположения и один слой карты, который рисует результат.
| Что это | |
|---|---|
createLocationManager() |
собственное позиционирование устройства |
createLocationSimulator(polyline) |
проигрывает маршрут |
createDummyLocationManager() |
позиции, которые задаёт ваш код |
createUserLocationLayer(mapWindow) |
стрелка, пин и круг точности на карте |
Разрешения запрашиваете вы
Обёртка не запрашивает разрешения на геолокацию — сделайте это платформенным способом до
подписки, иначе менеджер сообщит LocationStatus.NOT_AVAILABLE.
Подписка на позиции¶
class LocationController(private val mapKit: MapKit) {
private val locationManager = mapKit.createLocationManager()
private val locationListener = LocationListener(
onLocationUpdated = { location -> show(location.position) },
onLocationStatusUpdated = { status ->
if (status == LocationStatus.NOT_AVAILABLE) showUnavailable()
},
)
fun start() {
locationManager.subscribeForLocationUpdates(
subscriptionSettings = SubscriptionSettings(
useInBackground = UseInBackground.DISALLOW,
purpose = LocationPurpose.GENERAL,
),
locationListener = locationListener.asWeakRef(),
)
}
fun stop() {
locationManager.unsubscribe(locationListener.asWeakRef())
}
}
purpose сообщает MapKit, для чего нужна позиция — GENERAL, AUTOMOTIVE_NAVIGATION,
PEDESTRIAN_NAVIGATION, BICYCLE_NAVIGATION, SCOOTER_NAVIGATION, STATIC_DISPLAY_LOCATION, — а
useInBackground определяет, можно ли продолжать работу, когда приложение в фоне.
Одна позиция вместо потока:
locationManager.requestSingleUpdate(locationListener.asWeakRef())
suspend() и resume() приостанавливают и продолжают подписки, не снимая их.
Что несёт позиция¶
data class Location(
val position: Point,
val accuracy: Double?,
val altitude: Double?,
val altitudeAccuracy: Double?,
val heading: Double?,
val speed: Double?,
val indoorLevelId: String?,
val absoluteTimestamp: Instant,
val relativeTimestamp: Duration,
)
Всё, чего MapKit может не знать, — nullable. absoluteTimestamp — календарное время,
relativeTimestamp — монотонное время с момента загрузки устройства, именно по нему сравнивают
позиции.
Последняя позиция, которую получил MapKit, без всякой подписки:
val last: Location? = lastKnownLocation()
Рисование пользователя на карте¶
class UserLocationController(mapKit: MapKit, mapWindow: MapWindow) {
private val layer = mapKit.createUserLocationLayer(mapWindow)
private val tapListener = UserLocationTapListener { point -> openDetails(point) }
private val objectListener = UserLocationObjectListener(
onObjectAdded = { view ->
view.pin.setIcon(pinImage)
view.arrow.setIcon(arrowImage)
view.accuracyCircle.fillColor = Color.fromArgb(0x334CAF50)
},
onObjectRemoved = { },
onObjectUpdated = { _, _ -> },
)
init {
layer.isVisible = true
layer.isHeadingModeActive = true
layer.setTapListener(tapListener.asWeakRef())
layer.setObjectListener(objectListener.asWeakRef())
}
}
UserLocationView — это три объекта карты: arrow, pin и accuracyCircle, — поэтому к ним
применимо всё со страницы Объекты карты. onObjectAdded — место, где их
стилизуют; MapKit создаёт их лениво, после первой позиции.
Якорь¶
Режим якоря удерживает маркер пользователя в фиксированной точке экрана и двигает карту под ним — так выглядит навигация. Задаются два якоря: один для обычного состояния, второй — когда карта следует за курсом.
layer.setAnchor(
anchorNormal = PointF(width * 0.5f, height * 0.5f),
anchorCourse = PointF(width * 0.5f, height * 0.75f),
)
layer.isAutoZoomEnabled = true
resetAnchor() выходит из режима; isAnchorEnabled говорит, включён ли он. Любое движение камеры
пользователем его выключает, так что взводите заново, когда он снова нужен.
layer.cameraPosition — позиция, в которую слой сдвинул бы камеру, или null, пока позиции нет.
Другой источник для слоя¶
layer.setSource(simulator.asLocationManager().toLocationViewSource())
layer.setDefaultSource()
Симуляция маршрута¶
Симулятор проигрывает Polyline — для демо, для скриншотов и для тестов, которые не должны зависеть
от того, где находится устройство.
val route = Polyline(routePoints)
val simulator = mapKit.createLocationSimulator(route)
val settings = listOf(
SimulationSettings(
geometry = route,
locationSettings = LocationSettings(
provideSpeed = true,
speed = 20.0,
provideAccuracy = true,
accuracy = Range(from = 3.0, to = 5.0),
),
),
)
simulator.asLocationManager().resume()
simulator.startSimulation(settings)
Симулятор создаётся приостановленным
MapKit наследует LocationSimulator от LocationManager, и startSimulation его не
возобновляет. Без asLocationManager().resume() симуляция идёт, isActive остаётся false, и
до подписчиков ничего не доходит.
asLocationManager() — это то, что открывает subscribeForLocationUpdates, requestSingleUpdate,
unsubscribe, suspend и resume: Kotlin не может выразить наследование MapKit сразу на двух
платформах, поэтому связь записана как преобразование. Результат — обычный LocationManager, так
что его можно передать и в MapKit.setLocationManager(), и в toLocationViewSource().
private val simulatorListener = LocationSimulatorListener { println("route finished") }
simulator.subscribeForSimulatorEvents(simulatorListener.asWeakRef())
val progress = simulator.polylinePosition()
simulator.stopSimulation()
polylinePosition() — то, где на маршруте сейчас находится симуляция.
speed у симулятора устарел
MapKit перенёс это поле в LocationSettings. startSimulation целиком заменяет settings,
поэтому значение, присвоенное simulator.speed заранее, будет затёрто; задавайте
LocationSettings.speed вместе с provideSpeed = true в каждом SimulationSettings.
Что задаёт LocationSettings¶
Каждый флаг provideX решает, будет ли поле сообщаться вообще, а соседнее значение — что именно
сообщать:
LocationSettings(
provideAccuracy = true,
accuracy = Range(from = 3.0, to = 5.0),
provideSpeed = true,
speed = 20.0,
provideHeading = true,
headingError = Range(from = 0.0, to = 2.0),
locationTimeInterval = TimeInterval(from = 1.seconds, to = 2.seconds),
)
LocationSettings.fineSettings() и LocationSettings.coarseSettings() — готовые наборы от MapKit.
Задание позиций вручную¶
DummyLocationManager сообщает ровно то, что вы ему дали — полезно в тестах и для проигрывания
записанного трека в своём темпе.
val dummy = mapKit.createDummyLocationManager()
dummy.asLocationManager().subscribeForLocationUpdates(
subscriptionSettings = SubscriptionSettings(UseInBackground.DISALLOW, LocationPurpose.GENERAL),
locationListener = locationListener.asWeakRef(),
)
dummy.setLocation(
location = Location(
position = Point(55.751225, 37.629540),
accuracy = 5.0,
altitude = null,
altitudeAccuracy = null,
heading = 0.0,
speed = 0.0,
indoorLevelId = null,
absoluteTimestamp = Clock.System.now(),
relativeTimestamp = 0.seconds,
),
quality = DummyLocationQuality.HIGH,
)
Как и у симулятора, подписочные члены открывает asLocationManager().
Подмена глобального менеджера¶
MapKit.setLocationManager() заставляет все части MapKit — включая UserLocationLayer —
пользоваться вашим источником вместо устройства:
mapKit.setLocationManager(simulator.asLocationManager())
mapKit.resetLocationManagerToDefault()
В Compose¶
У YandexMap есть перегрузка, которая сама владеет слоем, так что создавать ничего не нужно:
@Composable
fun MapScreen() {
val locationState = rememberUserLocationState()
YandexMap(
locationState = locationState,
locationConfig = UserLocationConfig(
isVisible = true,
isHeadingEnabled = true,
isAutoZoomEnabled = true,
pin = UserLocationConfig.LocationIcon(image = pinImage),
arrow = UserLocationConfig.LocationIcon(image = arrowImage),
accuracy = UserLocationConfig.LocationAccuracy(
fillColor = Color(0x334CAF50),
),
),
modifier = Modifier.fillMaxSize(),
)
}
locationState.cameraPosition следует за UserLocationLayer.cameraPosition.