Skip to content

Draggable Marker

Note

You can find the full source code of this example in DraggableMarkerActivity.kt of the MapLibreAndroidTestApp.

Adding a marker on tap

Adding a tap listener to the map to add a marker on tap
maplibreMap.addOnMapClickListener {
    // Adding a marker on map click
    val features = maplibreMap.queryRenderedSymbols(it, layerId)
    if (features.isEmpty()) {
        addMarker(it)
    } else {
        // Displaying marker info on marker click
        Snackbar.make(
            mapView,
            "Marker's position: %.4f, %.4f".format(it.latitude, it.longitude),
            Snackbar.LENGTH_LONG
        )
            .show()
    }

    false
}

Allowing markers to be dragged

This is slightly more involved, as we implement it by implementing a DraggableSymbolsManager helper class.

This class is initialized and we pass a few callbacks when when markers are start or end being dragged.

draggableSymbolsManager = DraggableSymbolsManager(
    mapView,
    maplibreMap,
    featureCollection,
    source,
    layerId,
    actionBarHeight,
    0
)

// Adding symbol drag listeners
draggableSymbolsManager?.addOnSymbolDragListener(object : DraggableSymbolsManager.OnSymbolDragListener {
    override fun onSymbolDragStarted(id: String) {
        binding.draggedMarkerPositionTv.visibility = View.VISIBLE
        Snackbar.make(
            mapView,
            "Marker drag started (%s)".format(id),
            Snackbar.LENGTH_SHORT
        )
            .show()
    }

    override fun onSymbolDrag(id: String) {
        val point = featureCollection.features()?.find {
            it.id() == id
        }?.geometry() as Point
        binding.draggedMarkerPositionTv.text = "Dragged marker's position: %.4f, %.4f".format(point.latitude(), point.longitude())
    }

    override fun onSymbolDragFinished(id: String) {
        binding.draggedMarkerPositionTv.visibility = View.GONE
        Snackbar.make(
            mapView,
            "Marker drag finished (%s)".format(id),
            Snackbar.LENGTH_SHORT
        )
            .show()
    }
})

The implementation of DraggableSymbolsManager follows. In its initializer we define a handler for when a user long taps on a marker. This then starts dragging that marker. It does this by temporarily suspending all other gestures.

We create a custom implementation of MoveGestureDetector.OnMoveGestureListener and pass this to an instance of AndroidGesturesManager linked to the map view.

Tip

See maplibre-gestures-android for the implementation details of the gestures library used by MapLibre Android.

/**
 * A manager, that allows dragging symbols after they are long clicked.
 * Since this manager lives outside of the Maps SDK, we need to intercept parent's motion events
 * and pass them with [DraggableSymbolsManager.onParentTouchEvent].
 * If we were to try and overwrite [AppCompatActivity.onTouchEvent], those events would've been
 * consumed by the map.
 *
 * We also need to setup a [DraggableSymbolsManager.androidGesturesManager],
 * because after disabling map's gestures and starting the drag process
 * we still need to listen for move gesture events which map won't be able to provide anymore.
 *
 * @param mapView the mapView
 * @param maplibreMap the maplibreMap
 * @param symbolsCollection the collection that contains all the symbols that we want to be draggable
 * @param symbolsSource the source that contains the [symbolsCollection]
 * @param symbolsLayerId the ID of the layer that the symbols are displayed on
 * @param touchAreaShiftX X-axis padding that is applied to the parent's window motion event,
 * as that window can be bigger than the [mapView].
 * @param touchAreaShiftY Y-axis padding that is applied to the parent's window motion event,
 * as that window can be bigger than the [mapView].
 * @param touchAreaMaxX maximum value of X-axis motion event
 * @param touchAreaMaxY maximum value of Y-axis motion event
 */
class DraggableSymbolsManager(
    mapView: MapView,
    private val maplibreMap: MapLibreMap,
    private val symbolsCollection: FeatureCollection,
    private val symbolsSource: GeoJsonSource,
    private val symbolsLayerId: String,
    private val touchAreaShiftY: Int = 0,
    private val touchAreaShiftX: Int = 0,
    private val touchAreaMaxX: Int = mapView.width,
    private val touchAreaMaxY: Int = mapView.height
) {

    private val androidGesturesManager: AndroidGesturesManager = AndroidGesturesManager(mapView.context, false)
    private var draggedSymbolId: String? = null
    private val onSymbolDragListeners: MutableList<OnSymbolDragListener> = mutableListOf()

    init {
        maplibreMap.addOnMapLongClickListener {
            // Starting the drag process on long click
            draggedSymbolId = maplibreMap.queryRenderedSymbols(it, symbolsLayerId).firstOrNull()?.id()?.also { id ->
                maplibreMap.uiSettings.setAllGesturesEnabled(false)
                maplibreMap.gesturesManager.moveGestureDetector.interrupt()
                notifyOnSymbolDragListeners {
                    onSymbolDragStarted(id)
                }
            }
            false
        }

        androidGesturesManager.setMoveGestureListener(MyMoveGestureListener())
    }

    inner class MyMoveGestureListener : MoveGestureDetector.OnMoveGestureListener {
        override fun onMoveBegin(detector: MoveGestureDetector): Boolean {
            return true
        }

        override fun onMove(detector: MoveGestureDetector, distanceX: Float, distanceY: Float): Boolean {
            if (detector.pointersCount > 1) {
                // Stopping the drag when we don't work with a simple, on-pointer move anymore
                stopDragging()
                return true
            }

            // Updating symbol's position
            draggedSymbolId?.also { draggedSymbolId ->
                val moveObject = detector.getMoveObject(0)
                val point = PointF(moveObject.currentX - touchAreaShiftX, moveObject.currentY - touchAreaShiftY)

                if (point.x < 0 || point.y < 0 || point.x > touchAreaMaxX || point.y > touchAreaMaxY) {
                    stopDragging()
                }

                val latLng = maplibreMap.projection.fromScreenLocation(point)

                symbolsCollection.features()?.indexOfFirst {
                    it.id() == draggedSymbolId
                }?.also { index ->
                    symbolsCollection.features()?.get(index)?.also { oldFeature ->
                        val properties = oldFeature.properties()
                        val newFeature = Feature.fromGeometry(
                            Point.fromLngLat(latLng.longitude, latLng.latitude),
                            properties,
                            draggedSymbolId
                        )
                        symbolsCollection.features()?.set(index, newFeature)
                        symbolsSource.setGeoJson(symbolsCollection)
                        notifyOnSymbolDragListeners {
                            onSymbolDrag(draggedSymbolId)
                        }
                        return true
                    }
                }
            }

            return false
        }

        override fun onMoveEnd(detector: MoveGestureDetector, velocityX: Float, velocityY: Float) {
            // Stopping the drag when move ends
            stopDragging()
        }
    }

    private fun stopDragging() {
        maplibreMap.uiSettings.setAllGesturesEnabled(true)
        draggedSymbolId?.let {
            notifyOnSymbolDragListeners {
                onSymbolDragFinished(it)
            }
        }
        draggedSymbolId = null
    }

    fun onParentTouchEvent(ev: MotionEvent?) {
        androidGesturesManager.onTouchEvent(ev)
    }

    private fun notifyOnSymbolDragListeners(action: OnSymbolDragListener.() -> Unit) {
        onSymbolDragListeners.forEach(action)
    }

    fun addOnSymbolDragListener(listener: OnSymbolDragListener) {
        onSymbolDragListeners.add(listener)
    }

    fun removeOnSymbolDragListener(listener: OnSymbolDragListener) {
        onSymbolDragListeners.remove(listener)
    }

    interface OnSymbolDragListener {
        fun onSymbolDragStarted(id: String)
        fun onSymbolDrag(id: String)
        fun onSymbolDragFinished(id: String)
    }
}