Skip to content

Instantly share code, notes, and snippets.

@Humayung
Created June 24, 2024 03:30
Show Gist options
  • Save Humayung/63fce7f969156f39d6ea4576cc38dda1 to your computer and use it in GitHub Desktop.
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.
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}")
}
}
}
/**
* 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
}
}
}
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