-
Notifications
You must be signed in to change notification settings - Fork 177
Expand file tree
/
Copy pathCameraPositionState.kt
More file actions
355 lines (330 loc) · 14.3 KB
/
CameraPositionState.kt
File metadata and controls
355 lines (330 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// Copyright 2021 Google LLC
//
// 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 com.google.maps.android.compose
import androidx.annotation.UiThread
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.snapshots.StateFactoryMarker
import androidx.compose.runtime.staticCompositionLocalOf
import com.google.android.gms.maps.CameraUpdate
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.Projection
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.suspendCancellableCoroutine
import java.lang.Integer.MAX_VALUE
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Creates and remembers a [CameraPositionState] using [rememberSaveable].
*
* The camera position state is saved across configuration changes and process death,
* ensuring the map retains its last position.
*
* @param init A lambda that is called when the [CameraPositionState] is first created to
* configure its initial state, such as its position or zoom level.
*/
@Composable
public inline fun rememberCameraPositionState(
crossinline init: CameraPositionState.() -> Unit = {}
): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) {
CameraPositionState().apply(init)
}
/**
* Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver].
* [init] will be called when the [CameraPositionState] is first created to configure its
* initial state. Remember that the camera state can be applied when the map has been
* loaded.
*/
@Deprecated(
message = "The 'key' parameter is deprecated. Please use the new `rememberCameraPositionState` function without a key.",
replaceWith = ReplaceWith(
"rememberCameraPositionState(init)",
"com.google.maps.android.compose.rememberCameraPositionState"
)
)
@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 [GoogleMap] composable at a time
* as it reflects instance state for a single view of a map.
*
* @param position the initial camera position
*/
public class CameraPositionState private constructor(
position: CameraPosition = CameraPosition(LatLng(0.0, 0.0), 0f, 0f, 0f)
) {
/**
* 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))
}
}
}
// 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: GoogleMap? by mutableStateOf(null)
// An action to run when the map becomes available or unavailable.
// represents a mutually exclusive mutation to perform while holding `lock`.
// Guarded by `lock`.
private var onMapChanged: OnMapChangedCallback? by mutableStateOf(null)
/**
* Set [onMapChanged] to [callback], invoking the current callback's
* [OnMapChangedCallback.onCancelLocked] if one is present.
*/
private fun doOnMapChangedLocked(callback: OnMapChangedCallback) {
onMapChanged?.onCancelLocked()
onMapChanged = callback
}
// A token representing the current owner of any ongoing motion in progress.
// Used to determine if map animation should stop when calls to animate end.
// Guarded by `lock`.
private var movementOwner: Any? by mutableStateOf(null)
/**
* Used with [onMapChangedLocked] to execute one-time actions when a map becomes available
* or is made unavailable. Cancellation is provided in order to resume suspended coroutines
* that are awaiting the execution of one of these callbacks that will never come.
*/
private fun interface OnMapChangedCallback {
fun onMapChangedLocked(newMap: GoogleMap?)
fun onCancelLocked() {}
}
// The current map is set and cleared by side effect.
// There can be only one associated at a time.
internal fun setMap(map: GoogleMap?) {
synchronized(lock) {
if (this.map == null && map == null) return
if (this.map != null && map != null) {
error("CameraPositionState may only be associated with one GoogleMap at a time")
}
this.map = map
if (map == null) {
isMoving = false
} else {
map.moveCamera(CameraUpdateFactory.newCameraPosition(position))
}
onMapChanged?.let {
// Clear this first since the callback itself might set it again for later
onMapChanged = null
it.onMapChangedLocked(map)
}
}
}
/**
* Animate the camera position as specified by [update], returning once the animation has
* completed. [position] will reflect the position of the camera as the animation proceeds.
*
* [animate] will throw [CancellationException] if the animation does not fully complete.
* This can happen if:
*
* * The user manipulates the map directly
* * [position] is set explicitly, e.g. `state.position = CameraPosition(...)`
* * [animate] is called again before an earlier call to [animate] returns
* * [move] is called
* * The calling job is [cancelled][kotlinx.coroutines.Job.cancel] externally
*
* If this [CameraPositionState] is not currently bound to a [GoogleMap] this call will
* suspend until a map is bound and animation will begin.
*
* This method should only be called from a dispatcher bound to the map's UI thread.
*
* @param update the change that should be applied to the camera
* @param durationMs The duration of the animation in milliseconds. If [Int.MAX_VALUE] is
* provided, the default animation duration will be used. Otherwise, the value provided must be
* strictly positive, otherwise an [IllegalArgumentException] will be thrown.
*/
@UiThread
public suspend fun animate(update: CameraUpdate, durationMs: Int = MAX_VALUE) {
val myJob = currentCoroutineContext()[Job]
try {
suspendCancellableCoroutine<Unit> { continuation ->
synchronized(lock) {
movementOwner = myJob
val map = map
if (map == null) {
// Do it later
val animateOnMapAvailable = object : OnMapChangedCallback {
override fun onMapChangedLocked(newMap: GoogleMap?) {
if (newMap == null) {
// Cancel the animate caller and crash the map setter
@Suppress("ThrowableNotThrown")
continuation.resumeWithException(CancellationException(
"internal error; no GoogleMap available"))
error(
"internal error; no GoogleMap available to animate position"
)
}
performAnimateCameraLocked(newMap, update, durationMs, continuation)
}
override fun onCancelLocked() {
continuation.resumeWithException(
CancellationException("Animation cancelled")
)
}
}
doOnMapChangedLocked(animateOnMapAvailable)
continuation.invokeOnCancellation {
synchronized(lock) {
if (onMapChanged === animateOnMapAvailable) {
// External cancellation shouldn't invoke onCancel
// so we set this to null directly instead of going through
// doOnMapChangedLocked(null).
onMapChanged = null
}
}
}
} else {
performAnimateCameraLocked(map, update, durationMs, continuation)
}
}
}
} finally {
// continuation.invokeOnCancellation might be called from any thread, so stop the
// animation in progress here where we're guaranteed to be back on the right dispatcher.
synchronized(lock) {
if (myJob != null && movementOwner === myJob) {
movementOwner = null
map?.stopAnimation()
}
}
}
}
private fun performAnimateCameraLocked(
map: GoogleMap,
update: CameraUpdate,
durationMs: Int,
continuation: CancellableContinuation<Unit>
) {
val cancelableCallback = object : GoogleMap.CancelableCallback {
override fun onCancel() {
continuation.resumeWithException(CancellationException("Animation cancelled"))
}
override fun onFinish() {
continuation.resume(Unit)
}
}
if (durationMs == MAX_VALUE) {
map.animateCamera(update, cancelableCallback)
} else {
map.animateCamera(update, durationMs, cancelableCallback)
}
doOnMapChangedLocked {
check(it == null) {
"New GoogleMap unexpectedly set while an animation was still running"
}
map.stopAnimation()
}
}
/**
* Move the camera instantaneously as specified by [update]. Any calls to [animate] in progress
* will be cancelled. [position] will be updated when the bound map's position has been updated,
* or if the map is currently unbound, [update] will be applied when a map is next bound.
* Other calls to [move], [animate], or setting [position] will override an earlier pending
* call to [move].
*
* This method must be called from the map's UI thread.
*/
@UiThread
public fun move(update: CameraUpdate) {
synchronized(lock) {
val map = map
movementOwner = null
if (map == null) {
// Do it when we have a map available
doOnMapChangedLocked { it?.moveCamera(update) }
} else {
map.moveCamera(update)
}
}
}
public companion object {
/**
* Creates a new [CameraPositionState] object
*
* @param position the initial camera position
*/
@StateFactoryMarker
public operator fun invoke(
position: CameraPosition = CameraPosition(LatLng(0.0, 0.0), 0f, 0f, 0f)
): CameraPositionState = CameraPositionState(position)
/**
* The default saver implementation for [CameraPositionState]
*/
public val Saver: Saver<CameraPositionState, CameraPosition> = Saver(
save = { it.position },
restore = { CameraPositionState(it) }
)
}
}
/** Provides the [CameraPositionState] used by the map. */
internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() }
/** The current [CameraPositionState] used by the map. */
public val currentCameraPositionState: CameraPositionState
@[GoogleMapComposable ReadOnlyComposable Composable]
get() = LocalCameraPositionState.current