Skip to content

Commit 2476017

Browse files
committed
Implement camera callbacks
1 parent e57ef5d commit 2476017

7 files changed

Lines changed: 234 additions & 7 deletions

File tree

docs/PUBLIC_API.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,10 @@ Methods that must be forwarded from the parent Activity/Fragment.
163163
| `setOnCircleClickListener(OnCircleClickListener)` | `void` | NOT IMPLEMENTED | Not applicable |
164164
| `setOnGroundOverlayClickListener(OnGroundOverlayClickListener)` | `void` | NOT IMPLEMENTED | Not applicable |
165165
| `setOnPoiClickListener(OnPoiClickListener)` | `void` | NOT PLANNED | POI data not available in OSM tiles |
166-
| `setOnCameraMoveStartedListener(OnCameraMoveStartedListener)` | `void` | NOT IMPLEMENTED | Planned for future release |
167-
| `setOnCameraMoveListener(OnCameraMoveListener)` | `void` | NOT IMPLEMENTED | Planned for future release |
168-
| `setOnCameraIdleListener(OnCameraIdleListener)` | `void` | NOT IMPLEMENTED | Planned for future release |
169-
| `setOnCameraMoveCanceledListener(OnCameraMoveCanceledListener)` | `void` | NOT IMPLEMENTED | Planned for future release |
166+
| `setOnCameraMoveStartedListener(OnCameraMoveStartedListener)` | `void` | IMPLEMENTED | Tracks gesture, API, and developer-initiated moves |
167+
| `setOnCameraMoveListener(OnCameraMoveListener)` | `void` | IMPLEMENTED | Called repeatedly during camera movement |
168+
| `setOnCameraIdleListener(OnCameraIdleListener)` | `void` | IMPLEMENTED | Called when camera stops moving |
169+
| `setOnCameraMoveCanceledListener(OnCameraMoveCanceledListener)` | `void` | IMPLEMENTED | Called when animation is interrupted |
170170
| `setOnMapLoadedCallback(OnMapLoadedCallback)` | `void` | NOT IMPLEMENTED | Tiles load asynchronously, callback could be added |
171171
| `setInfoWindowAdapter(InfoWindowAdapter)` | `void` | NOT IMPLEMENTED | Info windows not yet implemented |
172172
| `setOnInfoWindowClickListener(OnInfoWindowClickListener)` | `void` | NOT IMPLEMENTED | Not applicable |

openmapview/src/main/kotlin/de/afarber/openmapview/MapController.kt

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ class MapController(
5656
private var animationJob: Job? = null
5757
private var animationListener: OnCameraAnimationListener? = null
5858

59+
var onCameraMoveStartedListener: OnCameraMoveStartedListener? = null
60+
var onCameraMoveListener: OnCameraMoveListener? = null
61+
var onCameraIdleListener: OnCameraIdleListener? = null
62+
var onCameraMoveCanceledListener: OnCameraMoveCanceledListener? = null
63+
64+
internal var isCameraMoving = false
65+
internal var currentMoveReason: Int? = null
66+
5967
private val scope = CoroutineScope(Dispatchers.Main + Job())
6068
private val tileDownloader = TileDownloader()
6169
private val tileCache = TileCache(context)
@@ -257,7 +265,20 @@ class MapController(
257265
fun moveCamera(cameraUpdate: CameraUpdate) {
258266
stopAnimation()
259267
commitPan()
268+
269+
// Fire camera move started event
270+
if (!isCameraMoving) {
271+
isCameraMoving = true
272+
currentMoveReason = OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
273+
onCameraMoveStartedListener?.onCameraMoveStarted(OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION)
274+
}
275+
260276
applyCameraUpdate(cameraUpdate)
277+
278+
// Fire camera idle event immediately since moveCamera is instant
279+
isCameraMoving = false
280+
currentMoveReason = null
281+
onCameraIdleListener?.onCameraIdle()
261282
}
262283

263284
/**
@@ -281,6 +302,13 @@ class MapController(
281302
val startPosition = getCameraPosition()
282303
val targetPosition = calculateTargetPosition(cameraUpdate, startPosition)
283304

305+
// Fire camera move started event
306+
if (!isCameraMoving) {
307+
isCameraMoving = true
308+
currentMoveReason = OnCameraMoveStartedListener.REASON_API_ANIMATION
309+
onCameraMoveStartedListener?.onCameraMoveStarted(OnCameraMoveStartedListener.REASON_API_ANIMATION)
310+
}
311+
284312
animationListener = listener
285313
animationJob =
286314
scope.launch {
@@ -314,6 +342,8 @@ class MapController(
314342
center = LatLng(interpolatedLat, interpolatedLng)
315343
zoom = interpolatedZoom
316344

345+
// Fire camera move event during animation
346+
onCameraMoveListener?.onCameraMove()
317347
onTileLoadedCallback?.invoke()
318348

319349
kotlinx.coroutines.delay(16)
@@ -324,6 +354,11 @@ class MapController(
324354
onTileLoadedCallback?.invoke()
325355

326356
animationListener?.onFinish()
357+
358+
// Fire camera idle event after animation completes
359+
isCameraMoving = false
360+
currentMoveReason = null
361+
onCameraIdleListener?.onCameraIdle()
327362
} catch (e: kotlinx.coroutines.CancellationException) {
328363
animationListener?.onCancel()
329364
throw e
@@ -341,9 +376,18 @@ class MapController(
341376
* onCancel() callback will be invoked. The camera remains at its current position.
342377
*/
343378
fun stopAnimation() {
344-
animationJob?.cancel()
345-
animationJob = null
346-
animationListener = null
379+
if (animationJob != null) {
380+
animationJob?.cancel()
381+
animationJob = null
382+
animationListener = null
383+
384+
// Fire camera move canceled event
385+
if (isCameraMoving) {
386+
onCameraMoveCanceledListener?.onCameraMoveCanceled()
387+
isCameraMoving = false
388+
currentMoveReason = null
389+
}
390+
}
347391
}
348392

349393
private fun applyCameraUpdate(cameraUpdate: CameraUpdate) {
@@ -459,6 +503,13 @@ class MapController(
459503
// Reset pan offset
460504
panOffsetX = 0f
461505
panOffsetY = 0f
506+
507+
// Fire camera idle event after pan gesture completes
508+
if (isCameraMoving && currentMoveReason == OnCameraMoveStartedListener.REASON_GESTURE) {
509+
isCameraMoving = false
510+
currentMoveReason = null
511+
onCameraIdleListener?.onCameraIdle()
512+
}
462513
}
463514

464515
/**
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
/**
11+
* Interface for receiving camera idle events.
12+
*
13+
* Implement this interface and set it using [OpenMapView.setOnCameraIdleListener]
14+
* to receive callbacks when the camera stops moving.
15+
*
16+
* This is called after all camera movements (gestures, animations) have completed.
17+
*/
18+
fun interface OnCameraIdleListener {
19+
/**
20+
* Called when the camera stops moving.
21+
*
22+
* This is invoked after animations complete or gestures end.
23+
*/
24+
fun onCameraIdle()
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
/**
11+
* Interface for receiving camera movement cancellation events.
12+
*
13+
* Implement this interface and set it using [OpenMapView.setOnCameraMoveCanceledListener]
14+
* to receive callbacks when camera movement is interrupted.
15+
*
16+
* This is called when an animation is canceled before completion, such as when
17+
* a new gesture starts during an animation or [OpenMapView.stopAnimation] is called.
18+
*/
19+
fun interface OnCameraMoveCanceledListener {
20+
/**
21+
* Called when camera movement is canceled.
22+
*
23+
* This occurs when an ongoing animation is interrupted.
24+
*/
25+
fun onCameraMoveCanceled()
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
/**
11+
* Interface for receiving camera movement events.
12+
*
13+
* Implement this interface and set it using [OpenMapView.setOnCameraMoveListener]
14+
* to receive callbacks while the camera is moving.
15+
*
16+
* This callback is invoked repeatedly during camera movement (gestures, animations).
17+
*/
18+
fun interface OnCameraMoveListener {
19+
/**
20+
* Called repeatedly while the camera is moving.
21+
*
22+
* This is called frequently during animations and gestures.
23+
*/
24+
fun onCameraMove()
25+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
/**
11+
* Interface for receiving camera movement start events.
12+
*
13+
* Implement this interface and set it using [OpenMapView.setOnCameraMoveStartedListener]
14+
* to receive callbacks when the camera starts moving.
15+
*/
16+
fun interface OnCameraMoveStartedListener {
17+
companion object {
18+
/**
19+
* Camera movement initiated by user gesture (pan, zoom, etc.)
20+
*/
21+
const val REASON_GESTURE = 1
22+
23+
/**
24+
* Camera movement initiated by [OpenMapView.animateCamera] call.
25+
*/
26+
const val REASON_API_ANIMATION = 2
27+
28+
/**
29+
* Camera movement initiated by [OpenMapView.moveCamera] call.
30+
*/
31+
const val REASON_DEVELOPER_ANIMATION = 3
32+
}
33+
34+
/**
35+
* Called when the camera starts moving.
36+
*
37+
* @param reason The reason for the camera movement. One of:
38+
* [REASON_GESTURE], [REASON_API_ANIMATION], [REASON_DEVELOPER_ANIMATION]
39+
*/
40+
fun onCameraMoveStarted(reason: Int)
41+
}

openmapview/src/main/kotlin/de/afarber/openmapview/OpenMapView.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,22 @@ class OpenMapView
155155

156156
// Pan the map if not dragging a marker
157157
if (!isDragging) {
158+
// Fire camera move started event on first pan movement
159+
if (movementDistance > 0 && !controller.isCameraMoving) {
160+
controller.isCameraMoving = true
161+
controller.currentMoveReason = de.afarber.openmapview.OnCameraMoveStartedListener.REASON_GESTURE
162+
controller.onCameraMoveStartedListener?.onCameraMoveStarted(
163+
de.afarber.openmapview.OnCameraMoveStartedListener.REASON_GESTURE,
164+
)
165+
}
166+
158167
controller.updatePanOffset(dx, dy)
168+
169+
// Fire camera move event during pan
170+
if (controller.isCameraMoving) {
171+
controller.onCameraMoveListener?.onCameraMove()
172+
}
173+
159174
lastTouchX = event.x
160175
lastTouchY = event.y
161176
invalidate()
@@ -574,6 +589,50 @@ class OpenMapView
574589
onMapLongClickListener = listener
575590
}
576591

592+
/**
593+
* Sets a listener to handle camera movement start events.
594+
*
595+
* Called when the camera starts moving, providing the reason for the movement.
596+
*
597+
* @param listener The listener to receive camera move started events, or null to clear the listener
598+
*/
599+
fun setOnCameraMoveStartedListener(listener: OnCameraMoveStartedListener?) {
600+
controller.onCameraMoveStartedListener = listener
601+
}
602+
603+
/**
604+
* Sets a listener to handle camera movement events.
605+
*
606+
* Called repeatedly while the camera is moving.
607+
*
608+
* @param listener The listener to receive camera move events, or null to clear the listener
609+
*/
610+
fun setOnCameraMoveListener(listener: OnCameraMoveListener?) {
611+
controller.onCameraMoveListener = listener
612+
}
613+
614+
/**
615+
* Sets a listener to handle camera idle events.
616+
*
617+
* Called when the camera stops moving after gestures or animations complete.
618+
*
619+
* @param listener The listener to receive camera idle events, or null to clear the listener
620+
*/
621+
fun setOnCameraIdleListener(listener: OnCameraIdleListener?) {
622+
controller.onCameraIdleListener = listener
623+
}
624+
625+
/**
626+
* Sets a listener to handle camera movement cancellation events.
627+
*
628+
* Called when an ongoing animation is interrupted before completion.
629+
*
630+
* @param listener The listener to receive camera move canceled events, or null to clear the listener
631+
*/
632+
fun setOnCameraMoveCanceledListener(listener: OnCameraMoveCanceledListener?) {
633+
controller.onCameraMoveCanceledListener = listener
634+
}
635+
577636
/**
578637
* Adds a polyline to the map.
579638
*

0 commit comments

Comments
 (0)