MapLibre compose wrapper library (#877)
Heavily inspired from https://github.com/googlemaps/android-maps-compose It doesn't aim to be a full featured library like android-maps-compose, it's been stripped down to only handle our use cases. Related to: https://github.com/vector-im/element-meta/issues/1674 https://github.com/vector-im/element-meta/issues/1682
This commit is contained in:
parent
4ff99e3c6b
commit
a852465554
17 changed files with 1133 additions and 6 deletions
|
|
@ -259,6 +259,9 @@ koverMerged {
|
||||||
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
|
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
|
||||||
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
|
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
|
||||||
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
|
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
|
||||||
|
excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
|
||||||
|
excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
|
||||||
|
excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
|
||||||
}
|
}
|
||||||
bound {
|
bound {
|
||||||
minValue = 90
|
minValue = 90
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.1.0"
|
statemachine = "com.freeletics.flowredux:compose:1.1.0"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
|
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
|
||||||
|
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
|
|
|
||||||
34
libraries/maplibre-compose/build.gradle.kts
Normal file
34
libraries/maplibre-compose/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.maplibre.compose"
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs += "-Xexplicit-api=strict"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(libs.maplibre)
|
||||||
|
api(libs.maplibre.ktx)
|
||||||
|
api(libs.maplibre.annotation)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.mapbox.mapboxsdk.location.modes.CameraMode as InternalCameraMode
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
public enum class CameraMode {
|
||||||
|
NONE,
|
||||||
|
NONE_COMPASS,
|
||||||
|
NONE_GPS,
|
||||||
|
TRACKING,
|
||||||
|
TRACKING_COMPASS,
|
||||||
|
TRACKING_GPS,
|
||||||
|
TRACKING_GPS_NORTH;
|
||||||
|
|
||||||
|
@InternalCameraMode.Mode
|
||||||
|
internal fun toInternal(): Int = when (this) {
|
||||||
|
NONE -> InternalCameraMode.NONE
|
||||||
|
NONE_COMPASS -> InternalCameraMode.NONE_COMPASS
|
||||||
|
NONE_GPS -> InternalCameraMode.NONE_GPS
|
||||||
|
TRACKING -> InternalCameraMode.TRACKING
|
||||||
|
TRACKING_COMPASS -> InternalCameraMode.TRACKING_COMPASS
|
||||||
|
TRACKING_GPS -> InternalCameraMode.TRACKING_GPS
|
||||||
|
TRACKING_GPS_NORTH -> InternalCameraMode.TRACKING_GPS_NORTH
|
||||||
|
}
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
fun fromInternal(@InternalCameraMode.Mode mode: Int): CameraMode = when (mode) {
|
||||||
|
InternalCameraMode.NONE -> NONE
|
||||||
|
InternalCameraMode.NONE_COMPASS -> NONE_COMPASS
|
||||||
|
InternalCameraMode.NONE_GPS -> NONE_GPS
|
||||||
|
InternalCameraMode.TRACKING -> TRACKING
|
||||||
|
InternalCameraMode.TRACKING_COMPASS -> TRACKING_COMPASS
|
||||||
|
InternalCameraMode.TRACKING_GPS -> TRACKING_GPS
|
||||||
|
InternalCameraMode.TRACKING_GPS_NORTH -> TRACKING_GPS_NORTH
|
||||||
|
else -> error("Unknown camera mode: $mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates the different reasons why the map camera started to move.
|
||||||
|
*
|
||||||
|
* Based on enum values from https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener.
|
||||||
|
*
|
||||||
|
* [NO_MOVEMENT_YET] is used as the initial state before any map movement has been observed.
|
||||||
|
*
|
||||||
|
* [UNKNOWN] is used to represent when an unsupported integer value is provided to [fromInt] - this
|
||||||
|
* may be a new constant value from the Maps SDK that isn't supported by maps-compose yet, in which
|
||||||
|
* case this library should be updated to include a new enum value for that constant.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
public enum class CameraMoveStartedReason(public val value: Int) {
|
||||||
|
UNKNOWN(-2),
|
||||||
|
NO_MOVEMENT_YET(-1),
|
||||||
|
GESTURE(REASON_API_GESTURE),
|
||||||
|
API_ANIMATION(REASON_API_ANIMATION),
|
||||||
|
DEVELOPER_ANIMATION(REASON_DEVELOPER_ANIMATION);
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
/**
|
||||||
|
* Converts from the Maps SDK [com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener]
|
||||||
|
* constants to [CameraMoveStartedReason], or returns [UNKNOWN] if there is no such
|
||||||
|
* [CameraMoveStartedReason] for the given [value].
|
||||||
|
*
|
||||||
|
* See https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener.
|
||||||
|
*/
|
||||||
|
public fun fromInt(value: Int): CameraMoveStartedReason {
|
||||||
|
return values().firstOrNull { it.value == value } ?: return UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import android.location.Location
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
|
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
|
import com.mapbox.mapboxsdk.maps.Projection
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver].
|
||||||
|
* [init] will be called when the [CameraPositionState] is first created to configure its
|
||||||
|
* initial state.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public inline fun rememberCameraPositionState(
|
||||||
|
key: String? = null,
|
||||||
|
crossinline init: CameraPositionState.() -> Unit = {}
|
||||||
|
): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) {
|
||||||
|
CameraPositionState().apply(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state object that can be hoisted to control and observe the map's camera state.
|
||||||
|
* A [CameraPositionState] may only be used by a single [MapboxMap] composable at a time
|
||||||
|
* as it reflects instance state for a single view of a map.
|
||||||
|
*
|
||||||
|
* @param position the initial camera position
|
||||||
|
* @param cameraMode the initial camera mode
|
||||||
|
*/
|
||||||
|
public class CameraPositionState(
|
||||||
|
position: CameraPosition = CameraPosition.Builder().build(),
|
||||||
|
cameraMode: CameraMode = CameraMode.NONE,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Whether the camera is currently moving or not. This includes any kind of movement:
|
||||||
|
* panning, zooming, or rotation.
|
||||||
|
*/
|
||||||
|
public var isMoving: Boolean by mutableStateOf(false)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason for the start of the most recent camera moment, or
|
||||||
|
* [CameraMoveStartedReason.NO_MOVEMENT_YET] if the camera hasn't moved yet or
|
||||||
|
* [CameraMoveStartedReason.UNKNOWN] if an unknown constant is received from the Maps SDK.
|
||||||
|
*/
|
||||||
|
public var cameraMoveStartedReason: CameraMoveStartedReason by mutableStateOf(
|
||||||
|
CameraMoveStartedReason.NO_MOVEMENT_YET
|
||||||
|
)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current [Projection] to be used for converting between screen
|
||||||
|
* coordinates and lat/lng.
|
||||||
|
*/
|
||||||
|
public val projection: Projection?
|
||||||
|
get() = map?.projection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local source of truth for the current camera position.
|
||||||
|
* While [map] is non-null this reflects the current position of [map] as it changes.
|
||||||
|
* While [map] is null it reflects the last known map position, or the last value set by
|
||||||
|
* explicitly setting [position].
|
||||||
|
*/
|
||||||
|
internal var rawPosition by mutableStateOf(position)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position of the camera on the map.
|
||||||
|
*/
|
||||||
|
public var position: CameraPosition
|
||||||
|
get() = rawPosition
|
||||||
|
set(value) {
|
||||||
|
synchronized(lock) {
|
||||||
|
val map = map
|
||||||
|
if (map == null) {
|
||||||
|
rawPosition = value
|
||||||
|
} else {
|
||||||
|
map.moveCamera(CameraUpdateFactory.newCameraPosition(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local source of truth for the current camera mode.
|
||||||
|
* While [map] is non-null this reflects the current camera mode as it changes.
|
||||||
|
* While [map] is null it reflects the last known camera mode, or the last value set by
|
||||||
|
* explicitly setting [cameraMode].
|
||||||
|
*/
|
||||||
|
internal var rawCameraMode by mutableStateOf(cameraMode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current tracking mode of the camera.
|
||||||
|
*/
|
||||||
|
public var cameraMode: CameraMode
|
||||||
|
get() = rawCameraMode
|
||||||
|
set(value) {
|
||||||
|
synchronized(lock) {
|
||||||
|
val map = map
|
||||||
|
if (map == null) {
|
||||||
|
rawCameraMode = value
|
||||||
|
} else {
|
||||||
|
map.locationComponent.cameraMode = value.toInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's last available location.
|
||||||
|
*/
|
||||||
|
public var location: Location? by mutableStateOf(null)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
// Used to perform side effects thread-safely.
|
||||||
|
// Guards all mutable properties that are not `by mutableStateOf`.
|
||||||
|
private val lock = Unit
|
||||||
|
|
||||||
|
// The map currently associated with this CameraPositionState.
|
||||||
|
// Guarded by `lock`.
|
||||||
|
private var map: MapboxMap? by mutableStateOf(null)
|
||||||
|
|
||||||
|
// The current map is set and cleared by side effect.
|
||||||
|
// There can be only one associated at a time.
|
||||||
|
internal fun setMap(map: MapboxMap?) {
|
||||||
|
synchronized(lock) {
|
||||||
|
if (this.map == null && map == null) return
|
||||||
|
if (this.map != null && map != null) {
|
||||||
|
error("CameraPositionState may only be associated with one MapboxMap at a time")
|
||||||
|
}
|
||||||
|
this.map = map
|
||||||
|
if (map == null) {
|
||||||
|
isMoving = false
|
||||||
|
} else {
|
||||||
|
map.moveCamera(CameraUpdateFactory.newCameraPosition(position))
|
||||||
|
map.locationComponent.cameraMode = cameraMode.toInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
/**
|
||||||
|
* The default saver implementation for [CameraPositionState].
|
||||||
|
*/
|
||||||
|
public val Saver: Saver<CameraPositionState, SaveableCameraPositionState> = Saver(
|
||||||
|
save = { SaveableCameraPositionState(it.position, it.cameraMode.toInternal()) },
|
||||||
|
restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides the [CameraPositionState] used by the map. */
|
||||||
|
internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() }
|
||||||
|
|
||||||
|
/** The current [CameraPositionState] used by the map. */
|
||||||
|
public val currentCameraPositionState: CameraPositionState
|
||||||
|
@[MapboxMapComposable ReadOnlyComposable Composable]
|
||||||
|
get() = LocalCameraPositionState.current
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
public data class SaveableCameraPositionState(
|
||||||
|
val position: CameraPosition,
|
||||||
|
val cameraMode: Int
|
||||||
|
) : Parcelable
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.mapbox.mapboxsdk.style.layers.Property
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
public enum class IconAnchor {
|
||||||
|
CENTER,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
TOP,
|
||||||
|
BOTTOM,
|
||||||
|
TOP_LEFT,
|
||||||
|
TOP_RIGHT,
|
||||||
|
BOTTOM_LEFT,
|
||||||
|
BOTTOM_RIGHT;
|
||||||
|
|
||||||
|
@Property.ICON_ANCHOR
|
||||||
|
internal fun toInternal(): String = when (this) {
|
||||||
|
CENTER -> Property.ICON_ANCHOR_CENTER
|
||||||
|
LEFT -> Property.ICON_ANCHOR_LEFT
|
||||||
|
RIGHT -> Property.ICON_ANCHOR_RIGHT
|
||||||
|
TOP -> Property.ICON_ANCHOR_TOP
|
||||||
|
BOTTOM -> Property.ICON_ANCHOR_BOTTOM
|
||||||
|
TOP_LEFT -> Property.ICON_ANCHOR_TOP_LEFT
|
||||||
|
TOP_RIGHT -> Property.ICON_ANCHOR_TOP_RIGHT
|
||||||
|
BOTTOM_LEFT -> Property.ICON_ANCHOR_BOTTOM_LEFT
|
||||||
|
BOTTOM_RIGHT -> Property.ICON_ANCHOR_BOTTOM_RIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.AbstractApplier
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
|
import com.mapbox.mapboxsdk.maps.Style
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||||
|
|
||||||
|
internal interface MapNode {
|
||||||
|
fun onAttached() {}
|
||||||
|
fun onRemoved() {}
|
||||||
|
fun onCleared() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object MapNodeRoot : MapNode
|
||||||
|
|
||||||
|
internal class MapApplier(
|
||||||
|
val map: MapboxMap,
|
||||||
|
val style: Style,
|
||||||
|
val symbolManager: SymbolManager,
|
||||||
|
) : AbstractApplier<MapNode>(MapNodeRoot) {
|
||||||
|
|
||||||
|
private val decorations = mutableListOf<MapNode>()
|
||||||
|
|
||||||
|
override fun onClear() {
|
||||||
|
symbolManager.deleteAll()
|
||||||
|
decorations.forEach { it.onCleared() }
|
||||||
|
decorations.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertBottomUp(index: Int, instance: MapNode) {
|
||||||
|
decorations.add(index, instance)
|
||||||
|
instance.onAttached()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertTopDown(index: Int, instance: MapNode) {
|
||||||
|
// insertBottomUp is preferred
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(from: Int, to: Int, count: Int) {
|
||||||
|
decorations.move(from, to, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(index: Int, count: Int) {
|
||||||
|
repeat(count) {
|
||||||
|
decorations[index + it].onRemoved()
|
||||||
|
}
|
||||||
|
decorations.remove(index, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
internal val DefaultMapLocationSettings = MapLocationSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for UI-related settings on the map.
|
||||||
|
*
|
||||||
|
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||||
|
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||||
|
*/
|
||||||
|
public data class MapLocationSettings(
|
||||||
|
public val locationEnabled: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
internal val DefaultMapSymbolManagerSettings = MapSymbolManagerSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for UI-related settings on the map.
|
||||||
|
*
|
||||||
|
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||||
|
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||||
|
*/
|
||||||
|
public data class MapSymbolManagerSettings(
|
||||||
|
public val iconAllowOverlap: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import android.view.Gravity
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
internal val DefaultMapUiSettings = MapUiSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for UI-related settings on the map.
|
||||||
|
*
|
||||||
|
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||||
|
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||||
|
*/
|
||||||
|
public data class MapUiSettings(
|
||||||
|
public val compassEnabled: Boolean = true,
|
||||||
|
public val rotationGesturesEnabled: Boolean = true,
|
||||||
|
public val scrollGesturesEnabled: Boolean = true,
|
||||||
|
public val tiltGesturesEnabled: Boolean = true,
|
||||||
|
public val zoomGesturesEnabled: Boolean = true,
|
||||||
|
public val logoGravity: Int = Gravity.BOTTOM,
|
||||||
|
public val attributionGravity: Int = Gravity.BOTTOM,
|
||||||
|
public val attributionTintColor: Color = Color.Unspecified,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
@file:Suppress("MatchingDeclarationName")
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ComposeNode
|
||||||
|
import androidx.compose.runtime.currentComposer
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
|
||||||
|
import com.mapbox.mapboxsdk.location.LocationComponentOptions
|
||||||
|
import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener
|
||||||
|
import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
|
import com.mapbox.mapboxsdk.maps.Style
|
||||||
|
|
||||||
|
private const val LOCATION_REQUEST_INTERVAL = 750L
|
||||||
|
|
||||||
|
internal class MapPropertiesNode(
|
||||||
|
val map: MapboxMap,
|
||||||
|
style: Style,
|
||||||
|
context: Context,
|
||||||
|
cameraPositionState: CameraPositionState,
|
||||||
|
) : MapNode {
|
||||||
|
|
||||||
|
init {
|
||||||
|
map.locationComponent.activateLocationComponent(
|
||||||
|
LocationComponentActivationOptions.Builder(context, style)
|
||||||
|
.locationComponentOptions(
|
||||||
|
LocationComponentOptions.builder(context)
|
||||||
|
.pulseEnabled(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.locationEngineRequest(
|
||||||
|
LocationEngineRequest.Builder(LOCATION_REQUEST_INTERVAL)
|
||||||
|
.setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
|
||||||
|
.setFastestInterval(LOCATION_REQUEST_INTERVAL)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
cameraPositionState.setMap(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cameraPositionState = cameraPositionState
|
||||||
|
set(value) {
|
||||||
|
if (value == field) return
|
||||||
|
field.setMap(null)
|
||||||
|
field = value
|
||||||
|
value.setMap(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttached() {
|
||||||
|
map.addOnCameraIdleListener {
|
||||||
|
cameraPositionState.isMoving = false
|
||||||
|
// addOnCameraIdleListener is only invoked when the camera position
|
||||||
|
// is changed via .animate(). To handle updating state when .move()
|
||||||
|
// is used, it's necessary to set the camera's position here as well
|
||||||
|
cameraPositionState.rawPosition = map.cameraPosition
|
||||||
|
// Updating user location on every camera move due to lack of a better location updates API.
|
||||||
|
cameraPositionState.location = map.locationComponent.lastKnownLocation
|
||||||
|
}
|
||||||
|
map.addOnCameraMoveCancelListener {
|
||||||
|
cameraPositionState.isMoving = false
|
||||||
|
}
|
||||||
|
map.addOnCameraMoveStartedListener {
|
||||||
|
cameraPositionState.cameraMoveStartedReason = CameraMoveStartedReason.fromInt(it)
|
||||||
|
cameraPositionState.isMoving = true
|
||||||
|
}
|
||||||
|
map.addOnCameraMoveListener {
|
||||||
|
cameraPositionState.rawPosition = map.cameraPosition
|
||||||
|
// Updating user location on every camera move due to lack of a better location updates API.
|
||||||
|
cameraPositionState.location = map.locationComponent.lastKnownLocation
|
||||||
|
}
|
||||||
|
map.locationComponent.addOnCameraTrackingChangedListener(object : OnCameraTrackingChangedListener {
|
||||||
|
override fun onCameraTrackingDismissed() {}
|
||||||
|
|
||||||
|
override fun onCameraTrackingChanged(currentMode: Int) {
|
||||||
|
cameraPositionState.rawCameraMode = CameraMode.fromInternal(currentMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved() {
|
||||||
|
cameraPositionState.setMap(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
cameraPositionState.setMap(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to keep the primary map properties up to date. This should never leave the map composition.
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
@Composable
|
||||||
|
internal inline fun MapUpdater(
|
||||||
|
cameraPositionState: CameraPositionState,
|
||||||
|
mapLocationSettings: MapLocationSettings,
|
||||||
|
mapUiSettings: MapUiSettings,
|
||||||
|
mapSymbolManagerSettings: MapSymbolManagerSettings,
|
||||||
|
) {
|
||||||
|
val mapApplier = currentComposer.applier as MapApplier
|
||||||
|
val map = mapApplier.map
|
||||||
|
val style = mapApplier.style
|
||||||
|
val symbolManager = mapApplier.symbolManager
|
||||||
|
val context = LocalContext.current
|
||||||
|
ComposeNode<MapPropertiesNode, MapApplier>(
|
||||||
|
factory = {
|
||||||
|
MapPropertiesNode(
|
||||||
|
map = map,
|
||||||
|
style = style,
|
||||||
|
context = context,
|
||||||
|
cameraPositionState = cameraPositionState,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
set(mapLocationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
|
||||||
|
|
||||||
|
set(mapUiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
|
||||||
|
set(mapUiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
|
||||||
|
set(mapUiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
|
||||||
|
set(mapUiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
|
||||||
|
set(mapUiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
|
||||||
|
set(mapUiSettings.logoGravity) { map.uiSettings.logoGravity = it }
|
||||||
|
set(mapUiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
|
||||||
|
set(mapUiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
|
||||||
|
|
||||||
|
set(mapSymbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
|
||||||
|
|
||||||
|
update(cameraPositionState) { this.cameraPositionState = it }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import android.content.ComponentCallbacks
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Composition
|
||||||
|
import androidx.compose.runtime.CompositionContext
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCompositionContext
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import com.mapbox.mapboxsdk.Mapbox
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
|
import com.mapbox.mapboxsdk.maps.Style
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compose container for a MapLibre [MapView].
|
||||||
|
*
|
||||||
|
* Heavily inspired by https://github.com/googlemaps/android-maps-compose
|
||||||
|
*
|
||||||
|
* @param styleUri a URI where to asynchronously fetch a style for the map
|
||||||
|
* @param modifier Modifier to be applied to the MapboxMap
|
||||||
|
* @param images images added to the map's style to be later used with [Symbol]
|
||||||
|
* @param cameraPositionState the [CameraPositionState] to be used to control or observe the map's
|
||||||
|
* camera state
|
||||||
|
* @param uiSettings the [MapUiSettings] to be used for UI-specific settings on the map
|
||||||
|
* @param symbolManagerSettings the [MapSymbolManagerSettings] to be used for symbol manager settings
|
||||||
|
* @param locationSettings the [MapLocationSettings] to be used for location settings
|
||||||
|
* @param content the content of the map
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun MapboxMap(
|
||||||
|
styleUri: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
images: ImmutableMap<String, Int> = persistentMapOf(),
|
||||||
|
cameraPositionState: CameraPositionState = rememberCameraPositionState(),
|
||||||
|
uiSettings: MapUiSettings = DefaultMapUiSettings,
|
||||||
|
symbolManagerSettings: MapSymbolManagerSettings = DefaultMapSymbolManagerSettings,
|
||||||
|
locationSettings: MapLocationSettings = DefaultMapLocationSettings,
|
||||||
|
content: (@Composable @MapboxMapComposable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
// When in preview, early return a Box with the received modifier preserving layout
|
||||||
|
if (LocalInspectionMode.current) {
|
||||||
|
@Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return.
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(Color.DarkGray)
|
||||||
|
) {
|
||||||
|
Text("[Map]", modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val mapView = remember {
|
||||||
|
Mapbox.getInstance(context)
|
||||||
|
MapView(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ModifierReused")
|
||||||
|
AndroidView(modifier = modifier, factory = { mapView })
|
||||||
|
MapLifecycle(mapView)
|
||||||
|
|
||||||
|
// rememberUpdatedState and friends are used here to make these values observable to
|
||||||
|
// the subcomposition without providing a new content function each recomposition
|
||||||
|
val currentCameraPositionState by rememberUpdatedState(cameraPositionState)
|
||||||
|
val currentUiSettings by rememberUpdatedState(uiSettings)
|
||||||
|
val currentMapLocationSettings by rememberUpdatedState(locationSettings)
|
||||||
|
val currentSymbolManagerSettings by rememberUpdatedState(symbolManagerSettings)
|
||||||
|
|
||||||
|
val parentComposition = rememberCompositionContext()
|
||||||
|
val currentContent by rememberUpdatedState(content)
|
||||||
|
|
||||||
|
LaunchedEffect(styleUri, images) {
|
||||||
|
disposingComposition {
|
||||||
|
parentComposition.newComposition(
|
||||||
|
context = context,
|
||||||
|
mapView = mapView,
|
||||||
|
styleUri = styleUri,
|
||||||
|
images = images,
|
||||||
|
) {
|
||||||
|
MapUpdater(
|
||||||
|
cameraPositionState = currentCameraPositionState,
|
||||||
|
mapUiSettings = currentUiSettings,
|
||||||
|
mapLocationSettings = currentMapLocationSettings,
|
||||||
|
mapSymbolManagerSettings = currentSymbolManagerSettings,
|
||||||
|
)
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalCameraPositionState provides cameraPositionState,
|
||||||
|
) {
|
||||||
|
currentContent?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun disposingComposition(factory: () -> Composition) {
|
||||||
|
val composition = factory()
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
composition.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun CompositionContext.newComposition(
|
||||||
|
context: Context,
|
||||||
|
mapView: MapView,
|
||||||
|
styleUri: String,
|
||||||
|
images: ImmutableMap<String, Int>,
|
||||||
|
noinline content: @Composable () -> Unit
|
||||||
|
): Composition {
|
||||||
|
val map = mapView.awaitMap()
|
||||||
|
val style = map.awaitStyle(context, styleUri, images)
|
||||||
|
val symbolManager = SymbolManager(mapView, map, style)
|
||||||
|
return Composition(
|
||||||
|
MapApplier(map, style, symbolManager), this
|
||||||
|
).apply {
|
||||||
|
setContent(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun MapView.awaitMap(): MapboxMap = suspendCoroutine { continuation ->
|
||||||
|
getMapAsync { map ->
|
||||||
|
continuation.resume(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun MapboxMap.awaitStyle(
|
||||||
|
context: Context,
|
||||||
|
styleUri: String,
|
||||||
|
images: ImmutableMap<String, Int>,
|
||||||
|
): Style = suspendCoroutine { continuation ->
|
||||||
|
setStyle(
|
||||||
|
Style.Builder().apply {
|
||||||
|
fromUri(styleUri)
|
||||||
|
images.forEach { (id, drawableRes) ->
|
||||||
|
withImage(id, checkNotNull(context.getDrawable(drawableRes)) {
|
||||||
|
"Drawable resource $drawableRes with id $id not found"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { style ->
|
||||||
|
continuation.resume(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers lifecycle observers to the local [MapView].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun MapLifecycle(mapView: MapView) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||||
|
val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) }
|
||||||
|
DisposableEffect(context, lifecycle, mapView) {
|
||||||
|
val mapLifecycleObserver = mapView.lifecycleObserver(previousState)
|
||||||
|
val callbacks = mapView.componentCallbacks()
|
||||||
|
|
||||||
|
lifecycle.addObserver(mapLifecycleObserver)
|
||||||
|
context.registerComponentCallbacks(callbacks)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
lifecycle.removeObserver(mapLifecycleObserver)
|
||||||
|
context.unregisterComponentCallbacks(callbacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DisposableEffect(mapView) {
|
||||||
|
onDispose {
|
||||||
|
mapView.onDestroy()
|
||||||
|
mapView.removeAllViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MapView.lifecycleObserver(previousState: MutableState<Lifecycle.Event>): LifecycleEventObserver =
|
||||||
|
LifecycleEventObserver { _, event ->
|
||||||
|
event.targetState
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_CREATE -> {
|
||||||
|
// Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in
|
||||||
|
// this case the MapboxMap composable also doesn't leave the composition. So,
|
||||||
|
// recreating the map does not restore state properly which must be avoided.
|
||||||
|
if (previousState.value != Lifecycle.Event.ON_STOP) {
|
||||||
|
this.onCreate(Bundle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Lifecycle.Event.ON_START -> this.onStart()
|
||||||
|
Lifecycle.Event.ON_RESUME -> this.onResume()
|
||||||
|
Lifecycle.Event.ON_PAUSE -> this.onPause()
|
||||||
|
Lifecycle.Event.ON_STOP -> this.onStop()
|
||||||
|
Lifecycle.Event.ON_DESTROY -> {
|
||||||
|
//handled in onDispose
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
previousState.value = event
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MapView.componentCallbacks(): ComponentCallbacks =
|
||||||
|
object : ComponentCallbacks {
|
||||||
|
override fun onConfigurationChanged(config: Configuration) {}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
this@componentCallbacks.onLowMemory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.ComposableTargetMarker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An annotation that can be used to mark a composable function as being expected to be use in a
|
||||||
|
* composable function that is also marked or inferred to be marked as a [MapboxMapComposable].
|
||||||
|
*
|
||||||
|
* This will produce build warnings when [MapboxMapComposable] composable functions are used outside
|
||||||
|
* of a [MapboxMapComposable] content lambda, and vice versa.
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@ComposableTargetMarker(description = "MapLibre Map Composable")
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.FILE,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.TYPE,
|
||||||
|
AnnotationTarget.TYPE_PARAMETER,
|
||||||
|
)
|
||||||
|
public annotation class MapboxMapComposable
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
* Copyright 2021 Google LLC
|
||||||
|
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.maplibre.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ComposeNode
|
||||||
|
import androidx.compose.runtime.currentComposer
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.Symbol
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||||
|
|
||||||
|
internal class SymbolNode(
|
||||||
|
val symbolManager: SymbolManager,
|
||||||
|
val symbol: Symbol,
|
||||||
|
) : MapNode {
|
||||||
|
override fun onRemoved() {
|
||||||
|
symbolManager.delete(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
symbolManager.delete(symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state object that can be hoisted to control and observe the symbol state.
|
||||||
|
*
|
||||||
|
* @param position the initial symbol position
|
||||||
|
*/
|
||||||
|
public class SymbolState(
|
||||||
|
position: LatLng = LatLng(0.0, 0.0)
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Current position of the symbol.
|
||||||
|
*/
|
||||||
|
public var position: LatLng by mutableStateOf(position)
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
/**
|
||||||
|
* The default saver implementation for [SymbolState].
|
||||||
|
*/
|
||||||
|
public val Saver: Saver<SymbolState, LatLng> = Saver(
|
||||||
|
save = { it.position },
|
||||||
|
restore = { SymbolState(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun rememberSymbolState(
|
||||||
|
key: String? = null,
|
||||||
|
position: LatLng = LatLng(0.0, 0.0)
|
||||||
|
): SymbolState = rememberSaveable(key = key, saver = SymbolState.Saver) {
|
||||||
|
SymbolState(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable for a symbol on the map.
|
||||||
|
*
|
||||||
|
* @param iconId an id of an image from the current [Style]
|
||||||
|
* @param state the [SymbolState] to be used to control or observe the symbol
|
||||||
|
* state such as its position and info window
|
||||||
|
* @param iconAnchor the anchor for the symbol image
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@MapboxMapComposable
|
||||||
|
public fun Symbol(
|
||||||
|
iconId: String,
|
||||||
|
state: SymbolState = rememberSymbolState(),
|
||||||
|
iconAnchor: IconAnchor? = null,
|
||||||
|
) {
|
||||||
|
val mapApplier = currentComposer.applier as MapApplier
|
||||||
|
val symbolManager = mapApplier.symbolManager
|
||||||
|
ComposeNode<SymbolNode, MapApplier>(
|
||||||
|
factory = {
|
||||||
|
SymbolNode(
|
||||||
|
symbolManager = symbolManager,
|
||||||
|
symbol = symbolManager.create(
|
||||||
|
SymbolOptions().apply {
|
||||||
|
withLatLng(state.position)
|
||||||
|
withIconImage(iconId)
|
||||||
|
iconAnchor?.let { withIconAnchor(it.toInternal()) }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
update(state.position) {
|
||||||
|
this.symbol.latLng = it
|
||||||
|
symbolManager.update(this.symbol)
|
||||||
|
}
|
||||||
|
update(iconId) {
|
||||||
|
this.symbol.iconImage = it
|
||||||
|
symbolManager.update(this.symbol)
|
||||||
|
}
|
||||||
|
update(iconAnchor) {
|
||||||
|
this.symbol.iconAnchor = it?.toInternal()
|
||||||
|
symbolManager.update(this.symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ Compose:
|
||||||
CompositionLocalAllowlist:
|
CompositionLocalAllowlist:
|
||||||
active: true
|
active: true
|
||||||
# You can optionally define a list of CompositionLocals that are allowed here
|
# You can optionally define a list of CompositionLocals that are allowed here
|
||||||
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher
|
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState
|
||||||
CompositionLocalNaming:
|
CompositionLocalNaming:
|
||||||
active: true
|
active: true
|
||||||
ContentEmitterReturningValues:
|
ContentEmitterReturningValues:
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
/\*
|
\/\*
|
||||||
(.*\n)* \* Copyright \(c\) 20\d\d New Vector Ltd(.*\n)*
|
(?:.*\n)* \* Copyright \(c\) 20\d\d New Vector Ltd
|
||||||
\*
|
(?:.*\n)* \*
|
||||||
\* Licensed under the Apache License, Version 2\.0 \(the "License"\);
|
\* Licensed under the Apache License, Version 2\.0 \(the "License"\);
|
||||||
\* you may not use this file except in compliance with the License\.
|
\* you may not use this file except in compliance with the License\.
|
||||||
\* You may obtain a copy of the License at
|
\* You may obtain a copy of the License at
|
||||||
\*
|
\*
|
||||||
\* http(s)?://www\.apache\.org/licenses/LICENSE-2\.0
|
\* http(?:s)?:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0
|
||||||
\*
|
\*
|
||||||
\* Unless required by applicable law or agreed to in writing, software
|
\* Unless required by applicable law or agreed to in writing, software
|
||||||
\* distributed under the License is distributed on an "AS IS" BASIS,
|
\* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
\* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
|
\* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
|
||||||
\* See the License for the specific language governing permissions and
|
\* See the License for the specific language governing permissions and
|
||||||
\* limitations under the License\.
|
\* limitations under the License\.
|
||||||
\*/
|
\*\/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue