diff --git a/app/build.gradle b/app/build.gradle index f1c2d6ba..8081923d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,10 +51,10 @@ dependencies { implementation deps.androidx.coreKtx implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'com.google.android.gms:play-services-maps:17.0.1' - implementation 'com.google.maps.android:maps-ktx:3.0.1' +// implementation 'com.google.maps.android:maps-ktx:3.0.1' implementation 'com.google.maps.android:maps-utils-ktx:3.0.1' -// implementation project(':maps-ktx') -// implementation project(':maps-utils-ktx') + implementation project(':maps-ktx') + // implementation project(':maps-utils-ktx') } secrets { @@ -64,4 +64,4 @@ secrets { // MAPS_API_KEY=YOUR_API_KEY propertiesFileName 'secure.properties' defaultPropertiesFileName 'secure.defaults.properties' -} \ No newline at end of file +} diff --git a/app/src/main/java/com/google/maps/android/ktx/demo/MainActivity.kt b/app/src/main/java/com/google/maps/android/ktx/demo/MainActivity.kt index f91d3b80..f6258360 100644 --- a/app/src/main/java/com/google/maps/android/ktx/demo/MainActivity.kt +++ b/app/src/main/java/com/google/maps/android/ktx/demo/MainActivity.kt @@ -46,6 +46,9 @@ import com.google.maps.android.ktx.awaitMap import com.google.maps.android.ktx.awaitMapLoad import com.google.maps.android.ktx.awaitSnapshot import com.google.maps.android.ktx.cameraEvents +import com.google.maps.android.ktx.awaitMap +import com.google.maps.android.ktx.cameraIdleEvents +import com.google.maps.android.ktx.cameraMoveStartedEvents import com.google.maps.android.ktx.demo.io.MyItemReader import com.google.maps.android.ktx.demo.model.MyItem import com.google.maps.android.ktx.model.cameraPosition @@ -54,6 +57,7 @@ import com.google.maps.android.ktx.utils.geojson.geoJsonLayer import com.google.maps.android.ktx.utils.kml.kmlLayer import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import org.json.JSONException /** @@ -94,15 +98,14 @@ class MainActivity : AppCompatActivity() { } showMapLayers(googleMap) addButtonClickListener(googleMap) - googleMap.cameraEvents().collect { event -> - when (event) { - is CameraIdleEvent -> Log.d(TAG, "Camera is idle.") - is CameraMoveCanceledEvent -> Log.d(TAG, "Camera move canceled") - is CameraMoveEvent -> Log.d(TAG, "Camera moved") - is CameraMoveStartedEvent -> Log.d( - TAG, - "Camera moved started. Reason: ${event.reason}" - ) + launch { + googleMap.cameraMoveStartedEvents().collect { + Log.d(TAG, "Camera moved.") + } + } + launch { + googleMap.cameraIdleEvents().collect { + Log.d(TAG, "Camera is idle.") } } } diff --git a/build.gradle b/build.gradle index a589d5e3..a73fc55c 100644 --- a/build.gradle +++ b/build.gradle @@ -18,11 +18,11 @@ buildscript { ext.versions = [ 'android' : [ "buildTools": "29.0.3", - "compileSdk": 29, + "compileSdk": 30, "minSdk" : 15, - "targetSdk" : 29 + "targetSdk" : 30 ], - 'androidMapsUtils' : '2.0.1', + 'androidMapsUtils' : '2.2.3', 'androidx' : [ 'appcompat': '1.1.0', 'coreKtx' : '1.2.0', @@ -36,7 +36,7 @@ buildscript { 'mapsBeta' : '3.1.0-beta', 'mockito' : '3.0.0', 'mockitoKotlin' : '2.2.0', - 'playServices' : '17.0.0', + 'playServices' : '17.0.1', 'volley' : '1.2.0', ] diff --git a/maps-ktx/src/main/java/com/google/maps/android/ktx/GoogleMap.kt b/maps-ktx/src/main/java/com/google/maps/android/ktx/GoogleMap.kt index dafd8e9b..f32ed461 100644 --- a/maps-ktx/src/main/java/com/google/maps/android/ktx/GoogleMap.kt +++ b/maps-ktx/src/main/java/com/google/maps/android/ktx/GoogleMap.kt @@ -18,6 +18,7 @@ package com.google.maps.android.ktx import android.graphics.Bitmap +import android.location.Location import androidx.annotation.IntDef import com.google.android.gms.maps.CameraUpdate import com.google.android.gms.maps.GoogleMap @@ -26,8 +27,11 @@ import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.GroundOverlayOptions +import com.google.android.gms.maps.model.IndoorBuilding +import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.gms.maps.model.PointOfInterest import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions import com.google.android.gms.maps.model.Polyline @@ -63,10 +67,49 @@ public object CameraMoveCanceledEvent : CameraEvent() public object CameraMoveEvent : CameraEvent() public data class CameraMoveStartedEvent(@MoveStartedReason val reason: Int) : CameraEvent() +/** + * Change event when a marker is dragged. See [GoogleMap.setOnMarkerDragListener] + */ +public sealed class OnMarkerDragEvent { + public abstract val marker: Marker +} + +/** + * Event emitted repeatedly while a marker is being dragged. + */ +public data class MarkerDragEvent(public override val marker: Marker) : OnMarkerDragEvent() + +/** + * Event emitted when a marker has finished being dragged. + */ +public data class MarkerDragEndEvent(public override val marker: Marker) : OnMarkerDragEvent() + +/** + * Event emitted when a marker starts being dragged. + */ +public data class MarkerDragStartEvent(public override val marker: Marker) : OnMarkerDragEvent() + +/** + * Change event when the indoor state changes. See [GoogleMap.OnIndoorStateChangeListener] + */ +public sealed class IndoorChangeEvent + +/** + * Change event when an indoor building is focused. + * See [GoogleMap.OnIndoorStateChangeListener.onIndoorBuildingFocused] + */ +public object IndoorBuildingFocusedEvent : IndoorChangeEvent() + +/** + * Change event when an indoor level is activated. + * See [GoogleMap.OnIndoorStateChangeListener.onIndoorLevelActivated] + */ +public data class IndoorLevelActivatedEvent(val building: IndoorBuilding) : IndoorChangeEvent() + // Since offer() can throw when the channel is closed (channel can close before the // block within awaitClose), wrap `offer` calls inside `runCatching`. // See: https://github.com/Kotlin/kotlinx.coroutines/issues/974 -private fun SendChannel.offerCatching(element: E): Boolean { +internal fun SendChannel.offerCatching(element: E): Boolean { return runCatching { offer(element) }.getOrDefault(false) } @@ -77,6 +120,9 @@ private fun SendChannel.offerCatching(element: E): Boolean { * [GoogleMap.setOnCameraMoveListener] and [GoogleMap.setOnCameraMoveStartedListener]. */ @ExperimentalCoroutinesApi +@Deprecated( + message = "Use cameraIdleEvents(), cameraMoveCanceledEvents(), cameraMoveEvents() or cameraMoveStartedEvents", +) public fun GoogleMap.cameraEvents(): Flow = callbackFlow { setOnCameraIdleListener { @@ -132,6 +178,52 @@ public suspend inline fun GoogleMap.awaitMapLoad(): Unit = } } +/** + * Returns a flow that emits when the camera is idle. Using this to observe camera idle events will + * override an existing listener (if any) to [GoogleMap.setOnCameraIdleListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.cameraIdleEvents(): Flow = + callbackFlow { + setOnCameraIdleListener { + offerCatching(Unit) + } + awaitClose { + setOnCameraIdleListener(null) + } + } + +/** + * Returns a flow that emits when a camera move is canceled. Using this to observe camera move + * cancel events will override an existing listener (if any) to + * [GoogleMap.setOnCameraMoveCanceledListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.cameraMoveCanceledEvents(): Flow = + callbackFlow { + setOnCameraMoveCanceledListener { + offerCatching(Unit) + } + awaitClose { + setOnCameraMoveCanceledListener(null) + } + } + +/** + * Returns a flow that emits when the camera moves. Using this to observe camera move events will + * override an existing listener (if any) to [GoogleMap.setOnCameraMoveListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.cameraMoveEvents(): Flow = + callbackFlow { + setOnCameraMoveListener { + offerCatching(Unit) + } + awaitClose { + setOnCameraMoveListener(null) + } + } + /** * A suspending function that returns a bitmap snapshot of the current view of the map. Uses * [GoogleMap.snapshot]. @@ -139,11 +231,276 @@ public suspend inline fun GoogleMap.awaitMapLoad(): Unit = * @param bitmap an optional preallocated bitmap * @return the snapshot */ -public suspend inline fun GoogleMap.awaitSnapshot(bitmap: Bitmap? = null): Bitmap = +public suspend inline fun GoogleMap.awaitSnapshot(bitmap: Bitmap? = null): Bitmap? = suspendCoroutine { continuation -> snapshot({ continuation.resume(it) }, bitmap) } +/** + * Returns a flow that emits when a camera move started. Using this to observe camera move start + * events will override an existing listener (if any) to [GoogleMap.setOnCameraMoveStartedListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.cameraMoveStartedEvents(): Flow = + callbackFlow { + setOnCameraMoveStartedListener { + offerCatching(Unit) + } + awaitClose { + setOnCameraMoveStartedListener(null) + } + } + +/** + * Returns a flow that emits when a circle is clicked. Using this to observe circle clicks events + * will override an existing listener (if any) to [GoogleMap.setOnCircleClickListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.circleClickEvents(): Flow = + callbackFlow { + setOnCircleClickListener { + offerCatching(it) + } + awaitClose { + setOnCircleClickListener(null) + } + } + +/** + * Returns a flow that emits when a ground overlay is clicked. Using this to observe ground overlay + * clicks events will override an existing listener (if any) to + * [GoogleMap.setOnGroundOverlayClickListener]. + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.groundOverlayClicks(): Flow = + callbackFlow { + setOnGroundOverlayClickListener { + offerCatching(it) + } + awaitClose { + setOnGroundOverlayClickListener(null) + } + } + +/** + * Returns a flow that emits when the indoor state changes. Using this to observe indoor state + * change events will override an existing listener (if any) to + * [GoogleMap.setOnIndoorStateChangeListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.indoorStateChangeEvents(): Flow = + callbackFlow { + setOnIndoorStateChangeListener(object : GoogleMap.OnIndoorStateChangeListener { + override fun onIndoorBuildingFocused() { + offerCatching(IndoorBuildingFocusedEvent) + } + + override fun onIndoorLevelActivated(indoorBuilding: IndoorBuilding) { + offerCatching(IndoorLevelActivatedEvent(building = indoorBuilding)) + } + }) + awaitClose { + setOnIndoorStateChangeListener(null) + } + } + +/** + * Returns a flow that emits when a marker's info window is clicked. Using this to observe info + * info window clicks will override an existing listener (if any) to + * [GoogleMap.setOnInfoWindowClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.infoWindowClickEvents(): Flow = + callbackFlow { + setOnInfoWindowClickListener { + offerCatching(it) + } + awaitClose { + setOnInfoWindowClickListener(null) + } + } + +/** + * Returns a flow that emits when a marker's info window is closed. Using this to observe info + * window closes will override an existing listener (if any) to + * [GoogleMap.setOnInfoWindowCloseListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.infoWindowCloseEvents(): Flow = + callbackFlow { + setOnInfoWindowCloseListener { + offerCatching(it) + } + awaitClose { + setOnInfoWindowCloseListener(null) + } + } + +/** + * Returns a flow that emits when a marker's info window is long pressed. Using this to observe info + * window long presses will override an existing listener (if any) to + * [GoogleMap.setOnInfoWindowLongClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.infoWindowLongClickEvents(): Flow = + callbackFlow { + setOnInfoWindowLongClickListener { + offerCatching(it) + } + awaitClose { + setOnInfoWindowLongClickListener(null) + } + } + +/** + * Returns a flow that emits when the map is clicked. Using this to observe map click events will + * override an existing listener (if any) to [GoogleMap.setOnMapClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.mapClickEvents(): Flow = + callbackFlow { + setOnMapClickListener { + offerCatching(it) + } + awaitClose { + setOnMapClickListener(null) + } + } + +/** + * Returns a flow that emits when the map is long clicked. Using this to observe map click events + * will override an existing listener (if any) to [GoogleMap.setOnMapLongClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.mapLongClickEvents(): Flow = + callbackFlow { + setOnMapLongClickListener { + offerCatching(it) + } + awaitClose { + setOnMapLongClickListener(null) + } + } + +/** + * Returns a flow that emits when a marker on the map is clicked. Using this to observe marker click + * events will override an existing listener (if any) to [GoogleMap.setOnMarkerClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.markerClickEvents(): Flow = + callbackFlow { + setOnMarkerClickListener { + offerCatching(it) + } + awaitClose { + setOnMarkerClickListener(null) + } + } + +/** + * Returns a flow that emits when a marker is dragged. Using this to observer marker drag events + * will override existing listeners (if any) to [GoogleMap.setOnMarkerDragListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.markerDragEvents(): Flow = + callbackFlow { + setOnMarkerDragListener(object : GoogleMap.OnMarkerDragListener { + override fun onMarkerDragStart(marker: Marker) { + offerCatching(MarkerDragStartEvent(marker = marker)) + } + + override fun onMarkerDrag(marker: Marker) { + offerCatching(MarkerDragEvent(marker = marker)) + } + + override fun onMarkerDragEnd(marker: Marker) { + offerCatching(MarkerDragEndEvent(marker = marker)) + } + + }) + awaitClose { + setOnMarkerDragListener(null) + } + } + +/** + * Returns a flow that emits when the my location button is clicked. Using this to observe my + * location button click events will override an existing listener (if any) to + * [GoogleMap.setOnMyLocationButtonClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.myLocationButtonClickEvents(): Flow = + callbackFlow { + setOnMyLocationButtonClickListener { + offerCatching(Unit) + } + awaitClose { + setOnMyLocationButtonClickListener(null) + } + } + +/** + * Returns a flow that emits when the my location blue dot is clicked. Using this to observe my + * location blue dot click events will override an existing listener (if any) to + * [GoogleMap.setOnMyLocationClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.myLocationClickEvents(): Flow = + callbackFlow { + setOnMyLocationClickListener { + offerCatching(it) + } + awaitClose { + setOnMyLocationClickListener(null) + } + } + +/** + * Returns a flow that emits when a PointOfInterest is clicked. Using this to observe + * PointOfInterest click events will override an existing listener (if any) to + * [GoogleMap.setOnPoiClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.poiClickEvents(): Flow = + callbackFlow { + setOnPoiClickListener { + offerCatching(it) + } + awaitClose { + setOnPoiClickListener(null) + } + } + +/** + * Returns a flow that emits when a Polygon is clicked. Using this to observe Polygon click events + * will override an existing listener (if any) to [GoogleMap.setOnPolygonClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.polygonClickEvents(): Flow = + callbackFlow { + setOnPolygonClickListener { + offerCatching(it) + } + awaitClose { + setOnPolygonClickListener(null) + } + } + +/** + * Returns a flow that emits when a Polyline is clicked. Using this to observe Polyline click events + * will override an existing listener (if any) to [GoogleMap.setOnPolylineClickListener] + */ +@ExperimentalCoroutinesApi +public fun GoogleMap.polylineClickEvents(): Flow = + callbackFlow { + setOnPolylineClickListener { + offerCatching(it) + } + awaitClose { + setOnPolylineClickListener(null) + } + } + /** * Builds a new [GoogleMapOptions] using the provided [optionsActions]. * @@ -170,7 +527,7 @@ public inline fun GoogleMap.addCircle(optionsActions: CircleOptions.() -> Unit): * * @return the added [Circle] */ -public inline fun GoogleMap.addGroundOverlay(optionsActions: GroundOverlayOptions.() -> Unit): GroundOverlay = +public inline fun GoogleMap.addGroundOverlay(optionsActions: GroundOverlayOptions.() -> Unit): GroundOverlay? = this.addGroundOverlay( groundOverlayOptions(optionsActions) ) @@ -180,7 +537,7 @@ public inline fun GoogleMap.addGroundOverlay(optionsActions: GroundOverlayOption * * @return the added [Marker] */ -public inline fun GoogleMap.addMarker(optionsActions: MarkerOptions.() -> Unit): Marker = +public inline fun GoogleMap.addMarker(optionsActions: MarkerOptions.() -> Unit): Marker? = this.addMarker( markerOptions(optionsActions) ) @@ -211,7 +568,7 @@ public inline fun GoogleMap.addPolyline(optionsActions: PolylineOptions.() -> Un * * @return the added [Polyline] */ -public inline fun GoogleMap.addTileOverlay(optionsActions: TileOverlayOptions.() -> Unit): TileOverlay = +public inline fun GoogleMap.addTileOverlay(optionsActions: TileOverlayOptions.() -> Unit): TileOverlay? = this.addTileOverlay( tileOverlayOptions(optionsActions) ) diff --git a/maps-ktx/src/main/java/com/google/maps/android/ktx/StreetViewPanoramaView.kt b/maps-ktx/src/main/java/com/google/maps/android/ktx/StreetViewPanoramaView.kt index 0d4c5d21..8ca2c331 100644 --- a/maps-ktx/src/main/java/com/google/maps/android/ktx/StreetViewPanoramaView.kt +++ b/maps-ktx/src/main/java/com/google/maps/android/ktx/StreetViewPanoramaView.kt @@ -2,6 +2,13 @@ package com.google.maps.android.ktx import com.google.android.gms.maps.StreetViewPanorama import com.google.android.gms.maps.StreetViewPanoramaView +import com.google.android.gms.maps.model.StreetViewPanoramaCamera +import com.google.android.gms.maps.model.StreetViewPanoramaLocation +import com.google.android.gms.maps.model.StreetViewPanoramaOrientation +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -18,4 +25,68 @@ public suspend inline fun StreetViewPanoramaView.awaitStreetViewPanorama(): Stre getStreetViewPanoramaAsync { continuation.resume(it) } + } + +/** + * Returns a flow that emits when the street view panorama camera changes. Using this to + * observe panorama camera change events will override an existing listener (if any) to + * [StreetViewPanorama.setOnStreetViewPanoramaCameraChangeListener]. + */ +@ExperimentalCoroutinesApi +public fun StreetViewPanorama.cameraChangeEvents(): Flow = + callbackFlow { + setOnStreetViewPanoramaCameraChangeListener { + offerCatching(it) + } + awaitClose { + setOnStreetViewPanoramaCameraChangeListener(null) + } + } + +/** + * Returns a flow that emits when the street view panorama loads a new panorama. Using this to + * observe panorama load change events will override an existing listener (if any) to + * [StreetViewPanorama.setOnStreetViewPanoramaChangeListener]. + */ +@ExperimentalCoroutinesApi +public fun StreetViewPanorama.changeEvents(): Flow = + callbackFlow { + setOnStreetViewPanoramaChangeListener { + offerCatching(it) + } + awaitClose { + setOnStreetViewPanoramaChangeListener(null) + } + } + +/** + * Returns a flow that emits when the street view panorama is clicked. Using this to + * observe panorama click events will override an existing listener (if any) to + * [StreetViewPanorama.setOnStreetViewPanoramaClickListener]. + */ +@ExperimentalCoroutinesApi +public fun StreetViewPanorama.clickEvents(): Flow = + callbackFlow { + setOnStreetViewPanoramaClickListener { + offerCatching(it) + } + awaitClose { + setOnStreetViewPanoramaClickListener(null) + } + } + +/** + * Returns a flow that emits when the street view panorama is long clicked. Using this to + * observe panorama long click events will override an existing listener (if any) to + * [StreetViewPanorama.setOnStreetViewPanoramaLongClickListener]. + */ +@ExperimentalCoroutinesApi +public fun StreetViewPanorama.longClickEvents(): Flow = + callbackFlow { + setOnStreetViewPanoramaLongClickListener { + offerCatching(it) + } + awaitClose { + setOnStreetViewPanoramaLongClickListener(null) + } } \ No newline at end of file