Skip to main content
Avalonia provides a built-in gesture recognition system that handles common touch and trackpad gestures like pinch, scroll, and pull. Gesture recognizers can be attached to any control to enable gesture-based interactions.

Gesture Recognizer Base Class

All gesture recognizers inherit from the GestureRecognizer base class, which provides the foundation for gesture detection.

Key Concepts

  • Target: The visual element that the gesture recognizer is attached to
  • Capture: Gesture recognizers can capture pointer input to track gestures
  • Prevention: Gestures can prevent other recognizers from processing the same input

Built-in Gesture Recognizers

PinchGestureRecognizer

Recognizes two-finger pinch and rotation gestures on touch devices.

Events

PinchEvent Raised continuously during a pinch gesture.
Scale
double
The scale factor relative to the initial distance between touch points. Values > 1.0 indicate zoom in, < 1.0 indicate zoom out.
Origin
Point
The center point between the two touch contacts, relative to the target element.
Angle
double
The current rotation angle in degrees.
AngleDelta
double
The change in rotation angle since the last event, in degrees.
PinchEndedEvent Raised when the pinch gesture ends (one or both fingers lifted).

ScrollGestureRecognizer

Recognizes scroll gestures from touch or trackpad input. ScrollGestureEvent
Delta
Vector
The scroll delta vector. Delta.X for horizontal scroll, Delta.Y for vertical scroll.
Id
int
Unique identifier for this scroll gesture session.
ScrollGestureEndedEvent Raised when the scroll gesture completes.

PullGestureRecognizer

Recognizes pull-to-refresh style gestures. PullGestureEvent
Delta
Vector
The pull distance vector.
PullDirection
PullDirection
The direction of the pull gesture: TopToBottom, BottomToTop, LeftToRight, or RightToLeft.
Id
int
Unique identifier for this pull gesture session.
PullGestureEndedEvent Raised when the pull gesture completes.

Usage Examples

Adding Pinch-to-Zoom

public class ZoomableControl : Control
{
    private double _scale = 1.0;
    private Point _scaleOrigin = new Point(0.5, 0.5);
    
    public ZoomableControl()
    {
        var pinchGesture = new PinchGestureRecognizer();
        GestureRecognizers.Add(pinchGesture);
    }
    
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        
        if (change.Property == PinchEvent)
        {
            var e = (PinchEventArgs)change.NewValue!;
            HandlePinch(e);
        }
        else if (change.Property == PinchEndedEvent)
        {
            HandlePinchEnded();
        }
    }
    
    private void HandlePinch(PinchEventArgs e)
    {
        // Update scale based on pinch gesture
        _scale = Math.Clamp(e.Scale, 0.5, 3.0);
        _scaleOrigin = e.Origin;
        
        InvalidateVisual();
        e.Handled = true;
    }
    
    public override void Render(DrawingContext context)
    {
        using (context.PushTransform(
            Matrix.CreateTranslation(-_scaleOrigin.X * Bounds.Width, 
                                      -_scaleOrigin.Y * Bounds.Height)
                  .Append(Matrix.CreateScale(_scale, _scale))
                  .Append(Matrix.CreateTranslation(_scaleOrigin.X * Bounds.Width,
                                                    _scaleOrigin.Y * Bounds.Height))))
        {
            // Render scaled content
            RenderContent(context);
        }
    }
}

Adding Pinch with Rotation

public class RotatableControl : Control
{
    private double _scale = 1.0;
    private double _rotation = 0.0;
    private Point _transformOrigin;
    
    public RotatableControl()
    {
        GestureRecognizers.Add(new PinchGestureRecognizer());
    }
    
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        
        if (change.Property == PinchEvent)
        {
            var e = (PinchEventArgs)change.NewValue!;
            
            // Update both scale and rotation
            _scale = e.Scale;
            _rotation += e.AngleDelta;
            _transformOrigin = e.Origin;
            
            InvalidateVisual();
        }
    }
    
    public override void Render(DrawingContext context)
    {
        // Apply rotation and scale transformations
        var matrix = Matrix.CreateTranslation(-_transformOrigin)
            .Append(Matrix.CreateRotation(_rotation * Math.PI / 180))
            .Append(Matrix.CreateScale(_scale, _scale))
            .Append(Matrix.CreateTranslation(_transformOrigin));
            
        using (context.PushTransform(matrix))
        {
            RenderContent(context);
        }
    }
}

Implementing Pull-to-Refresh

public class PullToRefreshControl : ContentControl
{
    private const double RefreshThreshold = 100;
    private double _pullDistance;
    private bool _isRefreshing;
    
    public PullToRefreshControl()
    {
        var pullGesture = new PullGestureRecognizer();
        GestureRecognizers.Add(pullGesture);
    }
    
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        
        if (change.Property == PullGestureEvent)
        {
            var e = (PullGestureEventArgs)change.NewValue!;
            HandlePullGesture(e);
        }
        else if (change.Property == PullGestureEndedEvent)
        {
            var e = (PullGestureEndedEventArgs)change.NewValue!;
            HandlePullEnded(e);
        }
    }
    
    private void HandlePullGesture(PullGestureEventArgs e)
    {
        if (e.PullDirection == PullDirection.TopToBottom && !_isRefreshing)
        {
            // Update pull distance
            _pullDistance = Math.Max(0, e.Delta.Y);
            
            // Show refresh indicator
            UpdateRefreshIndicator(_pullDistance);
            
            e.Handled = true;
        }
    }
    
    private void HandlePullEnded(PullGestureEndedEventArgs e)
    {
        if (e.PullDirection == PullDirection.TopToBottom)
        {
            if (_pullDistance >= RefreshThreshold && !_isRefreshing)
            {
                // Trigger refresh
                _isRefreshing = true;
                StartRefresh();
            }
            else
            {
                // Reset
                AnimateReset();
            }
            
            _pullDistance = 0;
        }
    }
    
    private async void StartRefresh()
    {
        // Perform refresh operation
        await RefreshDataAsync();
        
        _isRefreshing = false;
        AnimateReset();
    }
}

Custom Scroll Handling

public class CustomScrollControl : Control
{
    private Vector _scrollOffset;
    private int? _currentScrollGestureId;
    
    public CustomScrollControl()
    {
        GestureRecognizers.Add(new ScrollGestureRecognizer());
    }
    
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        
        if (change.Property == ScrollGestureEvent)
        {
            var e = (ScrollGestureEventArgs)change.NewValue!;
            HandleScroll(e);
        }
        else if (change.Property == ScrollGestureEndedEvent)
        {
            var e = (ScrollGestureEndedEventArgs)change.NewValue!;
            HandleScrollEnded(e);
        }
    }
    
    private void HandleScroll(ScrollGestureEventArgs e)
    {
        if (!_currentScrollGestureId.HasValue || 
            _currentScrollGestureId == e.Id)
        {
            _currentScrollGestureId = e.Id;
            
            // Apply scroll delta
            _scrollOffset += e.Delta;
            
            // Clamp scroll offset to valid range
            _scrollOffset = new Vector(
                Math.Clamp(_scrollOffset.X, -MaxScrollX, 0),
                Math.Clamp(_scrollOffset.Y, -MaxScrollY, 0)
            );
            
            InvalidateVisual();
            e.Handled = true;
        }
    }
    
    private void HandleScrollEnded(ScrollGestureEndedEventArgs e)
    {
        if (_currentScrollGestureId == e.Id)
        {
            _currentScrollGestureId = null;
            
            // Apply momentum or snap to position
            ApplyScrollMomentum();
        }
    }
}

XAML Usage

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <UserControl.GestureRecognizers>
        <PinchGestureRecognizer />
        <ScrollGestureRecognizer />
    </UserControl.GestureRecognizers>
    
    <Panel>
        <!-- Content -->
    </Panel>
</UserControl>

Preventing Gesture Recognition

You can prevent gesture recognizers from handling events:
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
    // Handle this event directly, prevent gestures
    if (ShouldHandleDirectly(e))
    {
        e.PreventGestureRecognition();
        HandlePointerDirectly(e);
        e.Handled = true;
    }
    
    base.OnPointerPressed(e);
}

Touchpad Gestures

Avalonia also supports trackpad gestures on desktop platforms:

Magnify Gesture

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
    base.OnPropertyChanged(change);
    
    if (change.Property == PointerTouchPadGestureMagnifyEvent)
    {
        var e = (PointerDeltaEventArgs)change.NewValue!;
        // e.Delta.X contains magnification delta
        Zoom(1.0 + e.Delta.X);
    }
}

Rotate Gesture

if (change.Property == PointerTouchPadGestureRotateEvent)
{
    var e = (PointerDeltaEventArgs)change.NewValue!;
    // e.Delta.X contains rotation delta in degrees
    Rotate(e.Delta.X);
}

Swipe Gesture

if (change.Property == PointerTouchPadGestureSwipeEvent)
{
    var e = (PointerDeltaEventArgs)change.NewValue!;
    // Handle swipe gesture
    if (Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y))
    {
        // Horizontal swipe
        NavigateHorizontal(e.Delta.X);
    }
    else
    {
        // Vertical swipe
        NavigateVertical(e.Delta.Y);
    }
}

Best Practices

  1. Add Recognizers in Constructor: Initialize gesture recognizers in the control constructor
  2. Check Gesture IDs: Use gesture IDs to track multi-gesture scenarios
  3. Handle Ended Events: Always handle gesture ended events to clean up state
  4. Prevent When Needed: Use PreventGestureRecognition() when handling raw pointer events
  5. Clamp Values: Apply sensible limits to scale, rotation, and scroll values
  6. Provide Feedback: Give visual feedback during gestures (progress indicators, etc.)
  7. Test on Devices: Test gestures on actual touch devices, not just with mouse
  8. Combine Recognizers: Multiple recognizers can coexist on the same control
  9. Mark as Handled: Set e.Handled = true in event handlers to prevent bubbling

Build docs developers (and LLMs) love