Created
June 24, 2024 03:30
-
-
Save Humayung/63fce7f969156f39d6ea4576cc38dda1 to your computer and use it in GitHub Desktop.
- This touch listener allows user to drag and drop marker on Google Maps without having to long press. This listener also record selected marker under on touch point using map.projection. This listener is used along with TouchableWrapper.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun example(){ | |
val map: GoogleMap = // your google maps | |
var selectedMarker: Marker? = null | |
OnMarkerEventListener.build( | |
map = map, | |
markers = { | |
// provide your interactable markers | |
emptyList() | |
}, | |
setSelectedMarker = {marker -> | |
// set selected marker from outside | |
selectedMarker = marker | |
}, | |
getSelectedMarker = { | |
// provide selected marker from outside | |
selectedMarker | |
} | |
){ | |
onMarkerDragStart { marker -> | |
println("drag started ${marker.position}") | |
} | |
onMarkerDrag {marker -> | |
println("dragging ${marker.position}") | |
} | |
onMarkerDragEnd {marker -> | |
println("drag ended ${marker.position}") | |
} | |
onSwipe { | |
// listen to marker swipe | |
// if return true, it will not respond to other event | |
false | |
} | |
onMarkerClicked { marker, isRelease -> | |
println("marker clicked ${marker.position}") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2024 http://github.com/humayung | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software | |
* and associated documentation files (the “Software”), to deal in the Software without restriction, | |
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, | |
* subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all copies or | |
* substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT | |
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE | |
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
package com.example.mapplayground.presentation.utils | |
import android.annotation.SuppressLint | |
import android.graphics.Point | |
import android.view.MotionEvent | |
import android.view.View | |
import androidx.core.graphics.minus | |
import androidx.core.graphics.plus | |
import com.google.android.gms.maps.GoogleMap | |
import com.google.android.gms.maps.model.LatLng | |
import com.google.android.gms.maps.model.Marker | |
import com.google.maps.android.SphericalUtil | |
import kotlin.math.sqrt | |
class OnMarkerEventListener private constructor( | |
private val map: GoogleMap, | |
private val onMarkerDrag: ((Marker) -> Unit)?, | |
private val onMarkerDragStart: ((Marker) -> Unit)?, | |
private val onMarkerClicked: ((Marker, isRelease: Boolean) -> Unit)?, | |
private val onMarkerDragEnd: ((Marker) -> Unit)?, | |
private val onGetMarkers: (() -> List<Marker>)?, | |
private val getSelectedMarker: (() -> Marker?)?, | |
private val setSelectedMarker: ((Marker?) -> Unit)?, | |
private val onSwipeStart: ((LatLng) -> Boolean)?, | |
private val onSwipeEnd: ((LatLng) -> Boolean)?, | |
private val onSwipe: ((LatLng) -> Boolean)? | |
) : View.OnTouchListener { | |
private var initialTouchPoint: Point? = null | |
private val prevTouchPoint: Point = Point(0, 0) | |
private var isDragging: Boolean = false | |
private var isSwiping: Boolean = false | |
private var draggingMarker: Marker? | |
set(value) = run { setSelectedMarker?.invoke(value) } | |
get() = getSelectedMarker?.invoke() | |
@SuppressLint("ClickableViewAccessibility") | |
override fun onTouch(v: View, event: MotionEvent): Boolean { | |
val touchPoint = Point(event.x.toInt(), event.y.toInt()) | |
when (event.action) { | |
MotionEvent.ACTION_DOWN -> { | |
initialTouchPoint = touchPoint | |
val markerOnScreen = getMarkerFromScreen(touchPoint) | |
if (markerOnScreen != null) return true | |
} | |
MotionEvent.ACTION_MOVE -> { | |
if (!isSwiping) { | |
isSwiping = true | |
val projection = map.projection | |
onSwipeStart?.invoke(projection.fromScreenLocation(touchPoint)) | |
} | |
val consumed = onSwipe?.invoke(map.projection.fromScreenLocation(touchPoint)) | |
if (consumed == true) return true | |
if (draggingMarker == null || onMarkerDrag == null) { | |
prevTouchPoint.set(event.x.toInt(), event.y.toInt()) | |
return false | |
} | |
draggingMarker?.let { marker: Marker -> | |
val projection = map.projection | |
if (isGreaterThanTouchTolerance(touchPoint)) { | |
if (!isDragging) { | |
onMarkerDragStart?.invoke(marker) | |
isDragging = true | |
} | |
val delta = getTouchPointDelta(touchPoint) | |
val currentPositionOnScreen = projection.toScreenLocation(marker.position) | |
// use this for different experience when dragging | |
val newPosition = currentPositionOnScreen.plus(delta) | |
marker.position = | |
projection.fromScreenLocation( | |
touchPoint // or newPosition | |
) | |
onMarkerDrag.invoke(marker) | |
} | |
prevTouchPoint.set(event.x.toInt(), event.y.toInt()) | |
return true | |
} | |
prevTouchPoint.set(event.x.toInt(), event.y.toInt()) | |
return true | |
} | |
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { | |
onSwipeEnd?.invoke(map.projection.fromScreenLocation(touchPoint)) | |
isSwiping = false | |
isDragging = false | |
if (!isGreaterThanTouchTolerance(touchPoint)) { | |
val prevMarker = draggingMarker | |
val markerOnScreen = getMarkerFromScreen(touchPoint) | |
draggingMarker?.let { marker -> | |
if (markerOnScreen == null) { | |
draggingMarker = null | |
onMarkerClicked?.invoke(marker, true) | |
return false | |
} | |
} | |
prevMarker?.let { marker -> | |
draggingMarker = null | |
onMarkerClicked?.invoke(marker, true) | |
} | |
if (prevMarker != markerOnScreen) { | |
draggingMarker = markerOnScreen | |
draggingMarker?.let { | |
onMarkerClicked?.invoke(it, false) | |
} | |
} | |
return false | |
} else { | |
draggingMarker?.let { marker -> | |
onMarkerDragEnd?.invoke(marker) | |
} | |
} | |
} | |
} | |
return false | |
} | |
private fun getTouchPointDelta(touchPoint: Point): Point { | |
return touchPoint.minus(prevTouchPoint) | |
} | |
private fun getMarkerFromScreen(touchPoint: Point): Marker? { | |
val projection = map.projection | |
val positionOnMap = | |
projection.fromScreenLocation(touchPoint) | |
return onGetMarkers?.invoke()?.filter { | |
SphericalUtil.computeDistanceBetween( | |
positionOnMap, | |
it.position | |
) < EARTH_TOLERANCE_DISTANCE | |
}?.minByOrNull { | |
SphericalUtil.computeDistanceBetween( | |
positionOnMap, | |
it.position | |
) | |
} | |
} | |
private fun isGreaterThanTouchTolerance(touchPoint: Point): Boolean { | |
return initialTouchPoint?.let { | |
it.dist(touchPoint) > SCREEN_DISTANCE_TOLERANCE | |
} ?: true | |
} | |
private fun Point.dist(other: Point): Double { | |
val deltaX = (this.x - other.x).toDouble() | |
val deltaY = (this.y - other.y).toDouble() | |
return sqrt(deltaX * deltaX + deltaY * deltaY) | |
} | |
companion object { | |
const val EARTH_TOLERANCE_DISTANCE = 1.5 | |
val SCREEN_DISTANCE_TOLERANCE = 20.dpToPx() | |
fun build( | |
map: GoogleMap, | |
markers: () -> List<Marker>, | |
setSelectedMarker: (Marker?) -> Unit, | |
getSelectedMarker: () -> Marker?, | |
factory: OnMarkerEventListenerBuilder.() -> Unit | |
): OnMarkerEventListener { | |
val builder = OnMarkerEventListenerBuilder() | |
factory(builder) | |
return OnMarkerEventListener( | |
map = map, | |
onMarkerDrag = builder._onMarkerDrag, | |
onMarkerDragStart = builder._onMarkerDragStart, | |
onMarkerClicked = builder._onMarkerClicked, | |
onMarkerDragEnd = builder._onMarkerDragEnd, | |
onSwipeStart = builder._onSwipeStart, | |
onSwipeEnd = builder._onSwapEnd, | |
onSwipe = builder._onSwipe, | |
onGetMarkers = markers, | |
getSelectedMarker = getSelectedMarker, | |
setSelectedMarker = setSelectedMarker | |
) | |
} | |
} | |
class OnMarkerEventListenerBuilder { | |
var _onMarkerDrag: ((Marker) -> Unit)? = null | |
private set | |
var _onMarkerDragStart: ((Marker) -> Unit)? = null | |
private set | |
var _onSwipeStart: ((LatLng) -> Boolean)? = null | |
private set | |
var _onSwapEnd: ((LatLng) -> Boolean)? = null | |
private set | |
var _onSwipe: ((LatLng) -> Boolean)? = null | |
private set | |
var _onMarkerClicked: ((Marker, isRelease: Boolean) -> Unit)? = null | |
private set | |
var _onMarkerDragEnd: ((Marker) -> Unit)? = null | |
private set | |
fun onMarkerDrag(block: (Marker) -> Unit) { | |
_onMarkerDrag = block | |
} | |
fun onSwipeEnd(block: (LatLng) -> Boolean) { | |
_onSwapEnd = block | |
} | |
fun onSwipe(block: (LatLng) -> Boolean) { | |
_onSwipe = block | |
} | |
fun onSwipeStart(block: (LatLng) -> Boolean) { | |
_onSwipeStart = block | |
} | |
fun onMarkerDragEnd(block: (Marker) -> Unit) { | |
_onMarkerDragEnd = block | |
} | |
fun onMarkerDragStart(block: (Marker) -> Unit) { | |
_onMarkerDragStart = block | |
} | |
fun onMarkerClicked(block: (Marker, isRelease: Boolean) -> Unit) { | |
_onMarkerClicked = block | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.mapplayground.presentation.utils | |
import android.content.Context | |
import android.util.AttributeSet | |
import android.view.MotionEvent | |
import android.view.View | |
import android.widget.FrameLayout | |
class TouchableWrapper(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { | |
var touchListener: OnTouchListener? = null | |
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { | |
val result = touchListener?.onTouch(this, ev) | |
return if (result == false) super.dispatchTouchEvent(ev) | |
else result == true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment