Browse Source
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/1682pull/890/head
Marco Romano
1 year ago
committed by
GitHub
17 changed files with 1133 additions and 6 deletions
@ -0,0 +1,34 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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) |
||||
} |
||||
} |
||||
) |
||||
} |
@ -1,15 +1,15 @@
@@ -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\. |
||||
\*/ |
||||
\*\/ |
||||
|
Loading…
Reference in new issue