Skip to main content
Avalonia provides comprehensive touch input support through the unified pointer event system. Touch events are handled through the same PointerEventArgs infrastructure used for mouse input, but with touch-specific features like multi-touch support.

Touch Device

The TouchDevice class handles raw touch events on a per-toplevel basis. It manages multiple simultaneous touch points and dispatches them as pointer events.

Key Features

  • Multi-touch Support: Each touch contact is represented by a unique IPointer instance
  • Automatic Capture: Touch contacts are automatically captured to the element they first touch
  • Double-Tap Detection: Built-in support for detecting double-tap gestures
  • Gesture Recognition: Integrates with gesture recognizers for pinch, scroll, and pull gestures

Touch Event Lifecycle

Touch events follow this lifecycle:
  1. TouchBeginPointerPressed event
  2. TouchUpdatePointerMoved event
  3. TouchEndPointerReleased event
  4. TouchCancel → Capture is released, no event raised

Pointer Types

Use the IPointer.Type property to distinguish input types:
  • PointerType.Touch - Touch input
  • PointerType.Mouse - Mouse input
  • PointerType.Pen - Pen/Stylus input

Usage Examples

Detecting Touch Input

protected override void OnPointerPressed(PointerPressedEventArgs e)
{
    if (e.Pointer.Type == PointerType.Touch)
    {
        // Handle touch-specific behavior
        var position = e.GetPosition(this);
        HandleTouchStart(position, e.Pointer.Id);
        e.Handled = true;
    }
    
    base.OnPointerPressed(e);
}

Multi-Touch Tracking

public class MultiTouchControl : Control
{
    private readonly Dictionary<int, Point> _activeTouches = new();
    
    protected override void OnPointerPressed(PointerPressedEventArgs e)
    {
        if (e.Pointer.Type == PointerType.Touch)
        {
            var position = e.GetPosition(this);
            _activeTouches[e.Pointer.Id] = position;
            
            // Handle multi-touch gesture
            if (_activeTouches.Count == 2)
            {
                StartTwoFingerGesture();
            }
            else if (_activeTouches.Count == 3)
            {
                StartThreeFingerGesture();
            }
            
            e.Handled = true;
        }
    }
    
    protected override void OnPointerMoved(PointerEventArgs e)
    {
        if (e.Pointer.Type == PointerType.Touch && 
            _activeTouches.ContainsKey(e.Pointer.Id))
        {
            var position = e.GetPosition(this);
            _activeTouches[e.Pointer.Id] = position;
            
            UpdateGesture();
            e.Handled = true;
        }
    }
    
    protected override void OnPointerReleased(PointerReleasedEventArgs e)
    {
        if (_activeTouches.Remove(e.Pointer.Id))
        {
            if (_activeTouches.Count == 0)
            {
                EndGesture();
            }
            e.Handled = true;
        }
    }
}

Detecting Double-Tap

protected override void OnPointerPressed(PointerPressedEventArgs e)
{
    if (e.Pointer.Type == PointerType.Touch)
    {
        if (e.ClickCount == 2)
        {
            // Double-tap detected
            OnDoubleTap(e.GetPosition(this));
            e.Handled = true;
        }
        else if (e.ClickCount == 1)
        {
            // Single tap
            OnSingleTap(e.GetPosition(this));
            e.Handled = true;
        }
    }
}

Touch Drawing with Pressure

private readonly Dictionary<int, List<PointerPoint>> _touchStrokes = new();

protected override void OnPointerPressed(PointerPressedEventArgs e)
{
    if (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)
    {
        var point = e.GetCurrentPoint(this);
        _touchStrokes[e.Pointer.Id] = new List<PointerPoint> { point };
        e.Handled = true;
    }
}

protected override void OnPointerMoved(PointerEventArgs e)
{
    if (_touchStrokes.TryGetValue(e.Pointer.Id, out var stroke))
    {
        var intermediatePoints = e.GetIntermediatePoints(this);
        stroke.AddRange(intermediatePoints);
        InvalidateVisual();
        e.Handled = true;
    }
}

protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
    if (_touchStrokes.ContainsKey(e.Pointer.Id))
    {
        _touchStrokes.Remove(e.Pointer.Id);
        e.Handled = true;
    }
}

public override void Render(DrawingContext context)
{
    foreach (var stroke in _touchStrokes.Values)
    {
        if (stroke.Count > 1)
        {
            for (int i = 0; i < stroke.Count - 1; i++)
            {
                var p1 = stroke[i];
                var p2 = stroke[i + 1];
                
                // Use pressure if available (pen input)
                var thickness = p1.Properties.Pressure > 0 
                    ? p1.Properties.Pressure * 10 
                    : 2;
                    
                var pen = new Pen(Brushes.Black, thickness);
                context.DrawLine(pen, p1.Position, p2.Position);
            }
        }
    }
}

Implementing Touch-to-Pan

public class PannableControl : Control
{
    private Point? _lastTouchPosition;
    private IPointer? _capturedPointer;
    private Matrix _transform = Matrix.Identity;
    
    protected override void OnPointerPressed(PointerPressedEventArgs e)
    {
        if (e.Pointer.Type == PointerType.Touch)
        {
            _lastTouchPosition = e.GetPosition(this);
            _capturedPointer = e.Pointer;
            e.Pointer.Capture(this);
            e.Handled = true;
        }
    }
    
    protected override void OnPointerMoved(PointerEventArgs e)
    {
        if (_capturedPointer == e.Pointer && _lastTouchPosition.HasValue)
        {
            var currentPosition = e.GetPosition(this);
            var delta = currentPosition - _lastTouchPosition.Value;
            
            // Apply pan transformation
            _transform = _transform.Append(Matrix.CreateTranslation(delta));
            _lastTouchPosition = currentPosition;
            
            InvalidateVisual();
            e.Handled = true;
        }
    }
    
    protected override void OnPointerReleased(PointerReleasedEventArgs e)
    {
        if (_capturedPointer == e.Pointer)
        {
            _lastTouchPosition = null;
            _capturedPointer = null;
            e.Pointer.Capture(null);
            e.Handled = true;
        }
    }
    
    public override void Render(DrawingContext context)
    {
        using (context.PushTransform(_transform))
        {
            // Render transformed content
            RenderContent(context);
        }
    }
}

Differentiating Between Touch and Mouse

protected override void OnPointerPressed(PointerPressedEventArgs e)
{
    switch (e.Pointer.Type)
    {
        case PointerType.Touch:
            // Touch-specific behavior (e.g., larger hit targets)
            HandleTouchInput(e);
            break;
            
        case PointerType.Mouse:
            // Mouse-specific behavior (e.g., hover effects)
            HandleMouseInput(e);
            break;
            
        case PointerType.Pen:
            // Pen-specific behavior (e.g., pressure sensitivity)
            HandlePenInput(e);
            break;
    }
}

Touch-Specific Properties

The PointerPointProperties class provides touch-specific information:

Pressure

var point = e.GetCurrentPoint(this);
var pressure = point.Properties.Pressure; // 0.0 to 1.0 (mainly for pen input)

Contact Area

var point = e.GetCurrentPoint(this);
if (point.Properties.ContactRect != null)
{
    // Touch contact area (if supported by hardware)
    var contactRect = point.Properties.ContactRect.Value;
}

Platform Considerations

Double-Tap Timing

Double-tap detection uses platform-specific timing:
var settings = GetPlatformSettings();
var doubleTapTime = settings.GetDoubleTapTime(PointerType.Touch);
var doubleTapSize = settings.GetDoubleTapSize(PointerType.Touch);

Touch Cancellation

Touch events can be cancelled by the system (e.g., when a system gesture is recognized):
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
    // Clean up when touch is cancelled
    if (_activeTouches.ContainsKey(e.Pointer.Id))
    {
        _activeTouches.Remove(e.Pointer.Id);
        CancelCurrentGesture();
    }
}

Best Practices

  1. Check Pointer Type: Always check e.Pointer.Type to handle touch differently from mouse when needed
  2. Track by Pointer ID: Use e.Pointer.Id to distinguish between multiple simultaneous touches
  3. Handle Cancellation: Implement OnPointerCaptureLost to handle system gesture interruptions
  4. Use Intermediate Points: Call GetIntermediatePoints() for smooth touch tracking
  5. Respect Platform Timing: Use platform settings for double-tap detection timing
  6. Don’t Rely on Hover: Touch devices don’t have hover states
  7. Provide Touch-Friendly Targets: Use larger hit areas for touch input (minimum 44x44 pixels recommended)
  8. Test Multi-Touch: Test your app with multiple simultaneous touch contacts
  9. Handle Pressure: Check Properties.Pressure for pen input to enable pressure-sensitive drawing

Build docs developers (and LLMs) love