Skip to content

Commit 004b86b

Browse files
author
Marco Romano
authored
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: element-hq/element-meta#1674 element-hq/element-meta#1682
1 parent 31331d1 commit 004b86b

File tree

17 files changed

+1133
-6
lines changed

17 files changed

+1133
-6
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ koverMerged {
259259
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
260260
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
261261
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
262+
excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
263+
excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
264+
excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
262265
}
263266
bound {
264267
minValue = 90

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
158158
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
159159
statemachine = "com.freeletics.flowredux:compose:1.1.0"
160160
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
161+
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0"
161162
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
162163

163164
# Analytics
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
plugins {
18+
id("io.element.android-compose-library")
19+
id("kotlin-parcelize")
20+
}
21+
22+
android {
23+
namespace = "io.element.android.libraries.maplibre.compose"
24+
25+
kotlinOptions {
26+
freeCompilerArgs += "-Xexplicit-api=strict"
27+
}
28+
}
29+
30+
dependencies {
31+
api(libs.maplibre)
32+
api(libs.maplibre.ktx)
33+
api(libs.maplibre.annotation)
34+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
* Copyright 2021 Google LLC
4+
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package io.element.android.libraries.maplibre.compose
20+
21+
import androidx.compose.runtime.Immutable
22+
import com.mapbox.mapboxsdk.location.modes.CameraMode as InternalCameraMode
23+
24+
@Immutable
25+
public enum class CameraMode {
26+
NONE,
27+
NONE_COMPASS,
28+
NONE_GPS,
29+
TRACKING,
30+
TRACKING_COMPASS,
31+
TRACKING_GPS,
32+
TRACKING_GPS_NORTH;
33+
34+
@InternalCameraMode.Mode
35+
internal fun toInternal(): Int = when (this) {
36+
NONE -> InternalCameraMode.NONE
37+
NONE_COMPASS -> InternalCameraMode.NONE_COMPASS
38+
NONE_GPS -> InternalCameraMode.NONE_GPS
39+
TRACKING -> InternalCameraMode.TRACKING
40+
TRACKING_COMPASS -> InternalCameraMode.TRACKING_COMPASS
41+
TRACKING_GPS -> InternalCameraMode.TRACKING_GPS
42+
TRACKING_GPS_NORTH -> InternalCameraMode.TRACKING_GPS_NORTH
43+
}
44+
45+
internal companion object {
46+
fun fromInternal(@InternalCameraMode.Mode mode: Int): CameraMode = when (mode) {
47+
InternalCameraMode.NONE -> NONE
48+
InternalCameraMode.NONE_COMPASS -> NONE_COMPASS
49+
InternalCameraMode.NONE_GPS -> NONE_GPS
50+
InternalCameraMode.TRACKING -> TRACKING
51+
InternalCameraMode.TRACKING_COMPASS -> TRACKING_COMPASS
52+
InternalCameraMode.TRACKING_GPS -> TRACKING_GPS
53+
InternalCameraMode.TRACKING_GPS_NORTH -> TRACKING_GPS_NORTH
54+
else -> error("Unknown camera mode: $mode")
55+
}
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
* Copyright 2021 Google LLC
4+
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package io.element.android.libraries.maplibre.compose
20+
21+
import androidx.compose.runtime.Immutable
22+
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION
23+
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE
24+
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
25+
26+
/**
27+
* Enumerates the different reasons why the map camera started to move.
28+
*
29+
* Based on enum values from https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener.
30+
*
31+
* [NO_MOVEMENT_YET] is used as the initial state before any map movement has been observed.
32+
*
33+
* [UNKNOWN] is used to represent when an unsupported integer value is provided to [fromInt] - this
34+
* may be a new constant value from the Maps SDK that isn't supported by maps-compose yet, in which
35+
* case this library should be updated to include a new enum value for that constant.
36+
*/
37+
@Immutable
38+
public enum class CameraMoveStartedReason(public val value: Int) {
39+
UNKNOWN(-2),
40+
NO_MOVEMENT_YET(-1),
41+
GESTURE(REASON_API_GESTURE),
42+
API_ANIMATION(REASON_API_ANIMATION),
43+
DEVELOPER_ANIMATION(REASON_DEVELOPER_ANIMATION);
44+
45+
public companion object {
46+
/**
47+
* Converts from the Maps SDK [com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener]
48+
* constants to [CameraMoveStartedReason], or returns [UNKNOWN] if there is no such
49+
* [CameraMoveStartedReason] for the given [value].
50+
*
51+
* See https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener.
52+
*/
53+
public fun fromInt(value: Int): CameraMoveStartedReason {
54+
return values().firstOrNull { it.value == value } ?: return UNKNOWN
55+
}
56+
}
57+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
* Copyright 2021 Google LLC
4+
* Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose)
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package io.element.android.libraries.maplibre.compose
20+
21+
import android.location.Location
22+
import android.os.Parcelable
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.ReadOnlyComposable
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
27+
import androidx.compose.runtime.saveable.Saver
28+
import androidx.compose.runtime.saveable.rememberSaveable
29+
import androidx.compose.runtime.setValue
30+
import androidx.compose.runtime.staticCompositionLocalOf
31+
import com.mapbox.mapboxsdk.camera.CameraPosition
32+
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
33+
import com.mapbox.mapboxsdk.maps.MapboxMap
34+
import com.mapbox.mapboxsdk.maps.Projection
35+
import kotlinx.parcelize.Parcelize
36+
37+
/**
38+
* Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver].
39+
* [init] will be called when the [CameraPositionState] is first created to configure its
40+
* initial state.
41+
*/
42+
@Composable
43+
public inline fun rememberCameraPositionState(
44+
key: String? = null,
45+
crossinline init: CameraPositionState.() -> Unit = {}
46+
): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) {
47+
CameraPositionState().apply(init)
48+
}
49+
50+
/**
51+
* A state object that can be hoisted to control and observe the map's camera state.
52+
* A [CameraPositionState] may only be used by a single [MapboxMap] composable at a time
53+
* as it reflects instance state for a single view of a map.
54+
*
55+
* @param position the initial camera position
56+
* @param cameraMode the initial camera mode
57+
*/
58+
public class CameraPositionState(
59+
position: CameraPosition = CameraPosition.Builder().build(),
60+
cameraMode: CameraMode = CameraMode.NONE,
61+
) {
62+
/**
63+
* Whether the camera is currently moving or not. This includes any kind of movement:
64+
* panning, zooming, or rotation.
65+
*/
66+
public var isMoving: Boolean by mutableStateOf(false)
67+
internal set
68+
69+
/**
70+
* The reason for the start of the most recent camera moment, or
71+
* [CameraMoveStartedReason.NO_MOVEMENT_YET] if the camera hasn't moved yet or
72+
* [CameraMoveStartedReason.UNKNOWN] if an unknown constant is received from the Maps SDK.
73+
*/
74+
public var cameraMoveStartedReason: CameraMoveStartedReason by mutableStateOf(
75+
CameraMoveStartedReason.NO_MOVEMENT_YET
76+
)
77+
internal set
78+
79+
/**
80+
* Returns the current [Projection] to be used for converting between screen
81+
* coordinates and lat/lng.
82+
*/
83+
public val projection: Projection?
84+
get() = map?.projection
85+
86+
/**
87+
* Local source of truth for the current camera position.
88+
* While [map] is non-null this reflects the current position of [map] as it changes.
89+
* While [map] is null it reflects the last known map position, or the last value set by
90+
* explicitly setting [position].
91+
*/
92+
internal var rawPosition by mutableStateOf(position)
93+
94+
/**
95+
* Current position of the camera on the map.
96+
*/
97+
public var position: CameraPosition
98+
get() = rawPosition
99+
set(value) {
100+
synchronized(lock) {
101+
val map = map
102+
if (map == null) {
103+
rawPosition = value
104+
} else {
105+
map.moveCamera(CameraUpdateFactory.newCameraPosition(value))
106+
}
107+
}
108+
}
109+
110+
/**
111+
* Local source of truth for the current camera mode.
112+
* While [map] is non-null this reflects the current camera mode as it changes.
113+
* While [map] is null it reflects the last known camera mode, or the last value set by
114+
* explicitly setting [cameraMode].
115+
*/
116+
internal var rawCameraMode by mutableStateOf(cameraMode)
117+
118+
/**
119+
* Current tracking mode of the camera.
120+
*/
121+
public var cameraMode: CameraMode
122+
get() = rawCameraMode
123+
set(value) {
124+
synchronized(lock) {
125+
val map = map
126+
if (map == null) {
127+
rawCameraMode = value
128+
} else {
129+
map.locationComponent.cameraMode = value.toInternal()
130+
}
131+
}
132+
}
133+
134+
/**
135+
* The user's last available location.
136+
*/
137+
public var location: Location? by mutableStateOf(null)
138+
internal set
139+
140+
// Used to perform side effects thread-safely.
141+
// Guards all mutable properties that are not `by mutableStateOf`.
142+
private val lock = Unit
143+
144+
// The map currently associated with this CameraPositionState.
145+
// Guarded by `lock`.
146+
private var map: MapboxMap? by mutableStateOf(null)
147+
148+
// The current map is set and cleared by side effect.
149+
// There can be only one associated at a time.
150+
internal fun setMap(map: MapboxMap?) {
151+
synchronized(lock) {
152+
if (this.map == null && map == null) return
153+
if (this.map != null && map != null) {
154+
error("CameraPositionState may only be associated with one MapboxMap at a time")
155+
}
156+
this.map = map
157+
if (map == null) {
158+
isMoving = false
159+
} else {
160+
map.moveCamera(CameraUpdateFactory.newCameraPosition(position))
161+
map.locationComponent.cameraMode = cameraMode.toInternal()
162+
}
163+
}
164+
}
165+
166+
public companion object {
167+
/**
168+
* The default saver implementation for [CameraPositionState].
169+
*/
170+
public val Saver: Saver<CameraPositionState, SaveableCameraPositionState> = Saver(
171+
save = { SaveableCameraPositionState(it.position, it.cameraMode.toInternal()) },
172+
restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) }
173+
)
174+
}
175+
}
176+
177+
/** Provides the [CameraPositionState] used by the map. */
178+
internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() }
179+
180+
/** The current [CameraPositionState] used by the map. */
181+
public val currentCameraPositionState: CameraPositionState
182+
@[MapboxMapComposable ReadOnlyComposable Composable]
183+
get() = LocalCameraPositionState.current
184+
185+
@Parcelize
186+
public data class SaveableCameraPositionState(
187+
val position: CameraPosition,
188+
val cameraMode: Int
189+
) : Parcelable

0 commit comments

Comments
 (0)