Skip to main content

Overview

Touch handlers in Open Mobile Maps manage user gestures including taps, pans, pinches, and other touch interactions. The SDK provides a default implementation with standard map controls, but you can customize or replace it entirely for specialized interactions.

TouchHandlerInterface

The core interface for handling touch events:
class TouchHandlerInterface {
public:
    virtual void onTouchEvent(const TouchEvent & touchEvent) = 0;
    virtual void insertListener(const std::shared_ptr<TouchInterface> & listener, int32_t index) = 0;
    virtual void addListener(const std::shared_ptr<TouchInterface> & listener) = 0;
    virtual void removeListener(const std::shared_ptr<TouchInterface> & listener) = 0;
}

Touch Event Structure

Touch events contain information about pointer positions and actions:
struct TouchEvent {
    std::vector<Vec2F> pointers;  // Positions of all active touch points
    float scrollDelta;             // Scroll wheel delta
    TouchAction touchAction;       // DOWN, MOVE, UP, or CANCEL
}

Touch Actions

  • DOWN - Touch began
  • MOVE - Touch moved
  • UP - Touch ended normally
  • CANCEL - Touch was cancelled (e.g., interrupted by system)

DefaultTouchHandler

The SDK includes a default touch handler with standard map interactions:

Creating the Default Handler

val touchHandler = DefaultTouchHandlerInterface.create(
    scheduler = mapView.scheduler,
    density = resources.displayMetrics.density
)

Touch Handler States

The default handler implements a state machine for gesture recognition:
enum TouchHandlingState {
    IDLE,
    ONE_FINGER_DOWN,
    ONE_FINGER_MOVING,
    ONE_FINGER_UP_AFTER_CLICK,
    ONE_FINGER_DOUBLE_CLICK_DOWN,
    ONE_FINGER_DOUBLE_CLICK_MOVE,
    TWO_FINGER_DOWN,
    TWO_FINGER_MOVING,
    ONE_FINGER_AFTER_TWO
}

Gesture Recognition

The default handler recognizes and processes:
  • Single tap - Click interaction
  • Long press - Press and hold (500ms default)
  • Pan - Single finger drag
  • Pinch zoom - Two finger zoom
  • Two finger pan - Two finger drag (rotation/tilt on 3D maps)
  • Double tap - Quick double click (300ms timeout)
  • Scroll - Mouse wheel zoom

Configuration Constants

int32_t TWO_FINGER_TOUCH_TIMEOUT = 100;    // ms to recognize two-finger gesture
int32_t DOUBLE_TAP_TIMEOUT = 300;          // ms between taps for double-tap
int32_t LONG_PRESS_TIMEOUT = 500;          // ms to trigger long press
int32_t CLICK_DISTANCE_MM = 3;             // Max movement to still be a click

Touch Listeners

Add custom listeners to respond to touch events:

TouchInterface

Implement this interface to receive touch callbacks:
class TouchInterface {
public:
    virtual bool onClickConfirmed(const Vec2F & coord) = 0;
    virtual bool onLongPress(const Vec2F & coord) = 0;
    virtual bool onMove(const Vec2F & deltaScreen, bool confirmed, bool doubleClick) = 0;
    virtual bool onTwoFingerClick(const Vec2F & coord1, const Vec2F & coord2) = 0;
    virtual bool onTwoFingerMove(const std::vector<Vec2F> & posScreen) = 0;
    virtual void clearTouch() = 0;
}

Adding Listeners

Listeners are managed in priority order:
// Add listener with highest priority (index 0)
touchHandler.insertListener(customListener, 0)

// Add listener at end
touchHandler.addListener(customListener)

// Remove listener
touchHandler.removeListener(customListener)
Listeners with lower index values have higher priority. Return true from a listener callback to consume the event and prevent lower-priority listeners from receiving it.

Platform-Specific Implementations

iOS Touch Handling

The iOS implementation bridges UIKit touch events to the touch handler:
class MCMapViewTouchHandler {
    // Convert UITouch events to MCTouchEvent
    func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchHandler.onTouchEvent(
            activeTouches.asMCTouchEvent(
                in: mapView,
                scale: Float(mapView.contentScaleFactor),
                action: .DOWN
            )
        )
    }
    
    func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchHandler.onTouchEvent(
            activeTouches.asMCTouchEvent(
                in: mapView,
                scale: Float(mapView.contentScaleFactor),
                action: .MOVE
            )
        )
    }
    
    // Gesture recognizer support
    func handlePan(panGestureRecognizer: UIPanGestureRecognizer) {
        let location = panGestureRecognizer.location(in: mapView)
        // Convert to touch event
    }
    
    func handlePinch(pinchGestureRecognizer: UIPinchGestureRecognizer) {
        // Simulate two-finger touch with scale
        let s = pow(pinchGestureRecognizer.scale, 2.414)
        // Convert to touch event with synthetic touch points
    }
}

Coordinate Conversion

iOS example of converting screen coordinates:
extension Set<UITouch> {
    func asMCTouchLocation(in view: UIView, scale: Float) -> [MCVec2F] {
        map {
            let location = $0.location(in: view)
            let x = Float(location.x) * scale
            let y = Float(location.y) * scale
            return MCVec2F(x: x, y: y)
        }
    }
}

Custom Touch Handler Example

Create a custom handler for specialized interactions:
class CustomTouchHandler(
    private val scheduler: SchedulerInterface,
    private val density: Float
) : TouchHandlerInterface {
    
    private val listeners = mutableListOf<TouchInterface>()
    private var lastTouchPosition: Vec2F? = null
    
    override fun onTouchEvent(touchEvent: TouchEvent) {
        when (touchEvent.touchAction) {
            TouchAction.DOWN -> handleTouchDown(touchEvent.pointers[0])
            TouchAction.MOVE -> handleTouchMove(touchEvent.pointers[0])
            TouchAction.UP -> handleTouchUp(touchEvent.pointers[0])
            TouchAction.CANCEL -> handleTouchCancel()
        }
    }
    
    private fun handleTouchDown(position: Vec2F) {
        lastTouchPosition = position
        // Custom touch down logic
    }
    
    private fun handleTouchMove(position: Vec2F) {
        lastTouchPosition?.let { last ->
            val delta = Vec2F(
                position.x - last.x,
                position.y - last.y
            )
            
            // Notify listeners
            for (listener in listeners) {
                if (listener.onMove(delta, true, false)) {
                    break  // Event consumed
                }
            }
        }
        lastTouchPosition = position
    }
    
    override fun addListener(listener: TouchInterface) {
        listeners.add(listener)
    }
    
    override fun removeListener(listener: TouchInterface) {
        listeners.remove(listener)
    }
    
    override fun insertListener(listener: TouchInterface, index: Int) {
        listeners.add(index, listener)
    }
}

Use Cases

Drawing Mode

Implement a custom touch handler for drawing on the map:
class DrawingTouchHandler : TouchHandlerInterface {
    private val drawingPoints = mutableListOf<Vec2F>()
    
    override fun onTouchEvent(touchEvent: TouchEvent) {
        when (touchEvent.touchAction) {
            TouchAction.DOWN -> {
                drawingPoints.clear()
                drawingPoints.add(touchEvent.pointers[0])
            }
            TouchAction.MOVE -> {
                drawingPoints.add(touchEvent.pointers[0])
                updateDrawingLayer(drawingPoints)
            }
            TouchAction.UP -> {
                finalizeDrawing(drawingPoints)
            }
        }
    }
}

Measurement Tool

Create a handler for distance measurement:
class MeasurementTouchListener : TouchInterface {
    private val measurementPoints = mutableListOf<Coord>()
    
    override fun onClickConfirmed(coord: Vec2F): Boolean {
        val mapCoord = convertToMapCoordinate(coord)
        measurementPoints.add(mapCoord)
        
        if (measurementPoints.size >= 2) {
            val distance = calculateDistance(measurementPoints)
            showMeasurementResult(distance)
        }
        
        return true  // Consume event
    }
}

Custom Gesture Recognition

Implement specialized gestures:
class CustomGestureHandler: MCTouchInterface {
    func onMove(_ deltaScreen: MCVec2F, confirmed: Bool, doubleClick: Bool) -> Bool {
        // Only respond to horizontal swipes
        if abs(deltaScreen.x) > abs(deltaScreen.y) * 2 {
            handleHorizontalSwipe(deltaScreen.x)
            return true
        }
        return false  // Let other handlers process
    }
    
    private func handleHorizontalSwipe(_ delta: Float) {
        if delta > 50 {
            // Swipe right action
        } else if delta < -50 {
            // Swipe left action
        }
    }
}

Best Practices

  • Return true from listener callbacks only when you want to prevent other handlers from processing the event
  • Use listener priority (index) to control which handlers get events first
  • Consider whether your custom interaction should block map navigation
  • Test interaction with multiple listeners to avoid conflicts
  • Keep touch event handlers lightweight - they run on every frame during interaction
  • Avoid heavy computations in onMove callbacks
  • Use debouncing for operations that don’t need to run on every touch event
  • Profile your touch handlers under sustained interaction (e.g., continuous panning)
  • Touch coordinates are in screen pixels (scaled by device pixel ratio)
  • Convert to map coordinates using the camera interface when needed
  • Account for screen scale/density when calculating distances
  • Test on different device sizes and pixel densities
  • Be aware of platform-specific gesture recognizers (especially on iOS)
  • Consider disabling default gestures when implementing custom interactions
  • Provide clear visual feedback during custom gestures
  • Test edge cases like interrupted gestures and multi-finger interactions

Styling

Customize colors, patterns, and visual properties

Layer Configuration

Configure tiled layer settings and zoom levels

Build docs developers (and LLMs) love