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.
The scale factor relative to the initial distance between touch points. Values > 1.0 indicate zoom in, < 1.0 indicate zoom out.
The center point between the two touch contacts, relative to the target element.
The current rotation angle in degrees.
The change in rotation angle since the last event, in degrees.
PinchEndedEvent
Raised when the pinch gesture ends (one or both fingers lifted).
Recognizes scroll gestures from touch or trackpad input.
ScrollGestureEvent
The scroll delta vector. Delta.X for horizontal scroll, Delta.Y for vertical scroll.
Unique identifier for this scroll gesture session.
ScrollGestureEndedEvent
Raised when the scroll gesture completes.
PullGestureRecognizer
Recognizes pull-to-refresh style gestures.
PullGestureEvent
The pull distance vector.
The direction of the pull gesture: TopToBottom, BottomToTop, LeftToRight, or RightToLeft.
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();
}
}
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
- Add Recognizers in Constructor: Initialize gesture recognizers in the control constructor
- Check Gesture IDs: Use gesture IDs to track multi-gesture scenarios
- Handle Ended Events: Always handle gesture ended events to clean up state
- Prevent When Needed: Use
PreventGestureRecognition() when handling raw pointer events
- Clamp Values: Apply sensible limits to scale, rotation, and scroll values
- Provide Feedback: Give visual feedback during gestures (progress indicators, etc.)
- Test on Devices: Test gestures on actual touch devices, not just with mouse
- Combine Recognizers: Multiple recognizers can coexist on the same control
- Mark as Handled: Set
e.Handled = true in event handlers to prevent bubbling