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:
@@ -259,6 +259,9 @@ koverMerged {
|
||||
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.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 {
|
||||
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" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.1.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"
|
||||
|
||||
# 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:
|
||||
active: true
|
||||
# You can optionally define a list of CompositionLocals that are allowed here
|
||||
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher
|
||||
allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState
|
||||
CompositionLocalNaming:
|
||||
active: true
|
||||
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"\);
|
||||
\* you may not use this file except in compliance with the License\.
|
||||
\* 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
|
||||
\* 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\.
|
||||
\*/
|
||||
\*\/
|
||||
|
||||
Reference in New Issue
Block a user