Enabling Smooth Scrolling and Zooming in Avalonia UI: Complete Implementation Guide
Introduction
Avalonia UI has emerged as a powerful cross-platform .NET UI framework, enabling developers to create beautiful desktop applications that run seamlessly on Windows, macOS, Linux, and even mobile platforms. However, out-of-the-box scrolling behavior can sometimes feel abrupt or unnatural, particularly when dealing with content-heavy applications or touch-enabled devices.
This comprehensive guide explores techniques for implementing smooth scrolling and zooming capabilities in Avalonia applications, significantly enhancing user experience and bringing your applications closer to the polished feel of native platform controls.
Understanding Avalonia's Scrolling Architecture
Default Scrolling Behavior
By default, Avalonia's scroll viewers implement immediate, direct scrolling that responds instantly to user input. While this approach offers precise control, it can feel jarring compared to the inertia-based, smoothed scrolling users expect from modern applications.
Key Characteristics of Default Scrolling:
- Immediate response to input
- No acceleration or deceleration
- Direct 1:1 mapping of input to scroll position
- Suitable for precision tasks but lacks polish
Why Smooth Scrolling Matters
Smooth scrolling provides several user experience benefits:
- Visual Comfort: Reduces eye strain during extended scrolling sessions
- Perceived Performance: Creates impression of responsive, polished application
- Touch Friendliness: Better matches touch gesture expectations
- Accessibility: Easier for users with motor control challenges
- Professional Appearance: Elevates overall application quality perception
Implementing Smooth Scrolling
Approach 1: Using Built-in Attached Properties
Avalonia provides some built-in support for smoothed scrolling through attached properties:
<ScrollViewer
x:Name="scrollViewer"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
av:ScrollViewer.IsSmoothScrollingEnabled="True">
<YourContentControl>
<!-- Your content here -->
</YourContentControl>
</ScrollViewer>Note: Availability of this property depends on your Avalonia version. Check documentation for your specific version's capabilities.
Approach 2: Custom Smooth Scrolling Behavior
For more control and consistent behavior across all Avalonia versions, implement custom smooth scrolling:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using System;
public class SmoothScrollBehavior : AvaloniaBehavior<ScrollViewer>
{
private double _targetOffset;
private double _currentOffset;
private bool _isAnimating;
private const double SmoothnessFactor = 0.15; // Adjust for desired smoothness
public static readonly AttachedProperty<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<SmoothScrollBehavior, ScrollViewer, bool>("IsEnabled");
public static void SetIsEnabled(ScrollViewer viewer, bool value) =>
viewer.SetValue(IsEnabledProperty, value);
public static bool GetIsEnabled(ScrollViewer viewer) =>
viewer.GetValue(IsEnabledProperty);
protected override void OnAttached()
{
base.OnAttached();
if (GetIsEnabled(AssociatedObject))
{
EnableSmoothScrolling();
}
}
private void EnableSmoothScrolling()
{
AssociatedObject.PropertyChanged += OnScrollViewerPropertyChanged;
}
private void OnScrollViewerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ScrollViewer.OffsetProperty)
{
var newOffset = (Vector)e.NewValue;
_targetOffset = newOffset.Y;
if (!_isAnimating)
{
_isAnimating = true;
StartSmoothScroll();
}
}
}
private async void StartSmoothScroll()
{
while (_isAnimating)
{
double difference = _targetOffset - _currentOffset;
if (Math.Abs(difference) < 0.5)
{
_currentOffset = _targetOffset;
AssociatedObject.Offset = new Vector(
AssociatedObject.Offset.X,
_currentOffset);
_isAnimating = false;
break;
}
_currentOffset += difference * SmoothnessFactor;
AssociatedObject.Offset = new Vector(
AssociatedObject.Offset.X,
_currentOffset);
await Dispatcher.UIThread.Yield();
}
}
protected override void OnDetaching()
{
AssociatedObject.PropertyChanged -= OnScrollViewerPropertyChanged;
base.OnDetaching();
}
}Usage in XAML
<Window
xmlns:local="clr-namespace:YourNamespace"
xmlns:behaviors="clr-namespace:YourNamespace.Behaviors">
<ScrollViewer behaviors:SmoothScrollBehavior.IsEnabled="True">
<StackPanel>
<!-- Your scrollable content -->
</StackPanel>
</ScrollViewer>
</Window>Implementing Inertia-Based Scrolling
For an even more natural feel, implement inertia-based scrolling that continues after user input stops:
public class InertiaScrollBehavior : AvaloniaBehavior<ScrollViewer>
{
private double _velocity;
private bool _isInertiaActive;
private const double Friction = 0.95; // Adjust for desired deceleration
private const double MinVelocity = 0.5;
private DispatcherTimer _inertiaTimer;
protected override void OnAttached()
{
base.OnAttached();
_inertiaTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(16) // ~60 FPS
};
_inertiaTimer.Tick += OnInertiaTick;
AssociatedObject.PointerReleased += OnPointerReleased;
}
private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
{
// Calculate velocity based on recent scroll movement
// This is simplified - implement proper velocity tracking
if (Math.Abs(_velocity) > MinVelocity)
{
_isInertiaActive = true;
_inertiaTimer.Start();
}
}
private void OnInertiaTick(object sender, EventArgs e)
{
if (!_isInertiaActive)
{
_inertiaTimer.Stop();
return;
}
_velocity *= Friction;
if (Math.Abs(_velocity) < MinVelocity)
{
_isInertiaActive = false;
_inertiaTimer.Stop();
return;
}
var currentOffset = AssociatedObject.Offset;
AssociatedObject.Offset = new Vector(
currentOffset.X,
currentOffset.Y + _velocity);
}
protected override void OnDetaching()
{
AssociatedObject.PointerReleased -= OnPointerReleased;
_inertiaTimer?.Stop();
base.OnDetaching();
}
}Implementing Zoom Functionality
Basic Zoom Implementation
Add zoom capabilities using mouse wheel or pinch gestures:
public class ZoomBehavior : AvaloniaBehavior<Control>
{
public static readonly AttachedProperty<double> ZoomFactorProperty =
AvaloniaProperty.RegisterAttached<ZoomBehavior, Control, double>("ZoomFactor", 1.0);
public static readonly AttachedProperty<double> MinZoomProperty =
AvaloniaProperty.RegisterAttached<ZoomBehavior, Control, double>("MinZoom", 0.5);
public static readonly AttachedProperty<double> MaxZoomProperty =
AvaloniaProperty.RegisterAttached<ZoomBehavior, Control, double>("MaxZoom", 3.0);
public static void SetZoomFactor(Control control, double value) =>
control.SetValue(ZoomFactorProperty, value);
public static double GetZoomFactor(Control control) =>
control.GetValue(ZoomFactorProperty);
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PointerWheelChanged += OnPointerWheelChanged;
}
private void OnPointerWheelChanged(object sender, PointerWheelEventArgs e)
{
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
double currentZoom = GetZoomFactor(AssociatedObject);
double zoomDelta = e.Delta.Y > 0 ? 0.1 : -0.1;
double newZoom = Math.Max(
GetMinZoom(AssociatedObject),
Math.Min(GetMaxZoom(AssociatedObject), currentZoom + zoomDelta));
SetZoomFactor(AssociatedObject, newZoom);
// Apply zoom using ScaleTransform
if (AssociatedObject.RenderTransform is ScaleTransform scaleTransform)
{
scaleTransform.ScaleX = newZoom;
scaleTransform.ScaleY = newZoom;
}
else
{
AssociatedObject.RenderTransform = new ScaleTransform(newZoom, newZoom);
AssociatedObject.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
}
e.Handled = true;
}
}
protected override void OnDetaching()
{
AssociatedObject.PointerWheelChanged -= OnPointerWheelChanged;
base.OnDetaching();
}
}XAML Usage for Zoom
<Border
behaviors:ZoomBehavior.ZoomFactor="1.0"
behaviors:ZoomBehavior.MinZoom="0.5"
behaviors:ZoomBehavior.MaxZoom="3.0">
<YourContentControl>
<!-- Zoomable content -->
</YourContentControl>
</Border>Touch-Friendly Scrolling
Implementing Touch Gestures
For touch-enabled devices, implement proper gesture handling:
public class TouchScrollBehavior : AvaloniaBehavior<ScrollViewer>
{
private Point _lastTouchPoint;
private bool _isTouching;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TouchBegin += OnTouchBegin;
AssociatedObject.TouchMove += OnTouchMove;
AssociatedObject.TouchEnd += OnTouchEnd;
}
private void OnTouchBegin(object sender, TouchEventArgs e)
{
_isTouching = true;
_lastTouchPoint = e.GetTouchPoint(AssociatedObject).Position;
}
private void OnTouchMove(object sender, TouchEventArgs e)
{
if (!_isTouching) return;
var currentPoint = e.GetTouchPoint(AssociatedObject).Position;
double deltaY = _lastTouchPoint.Y - currentPoint.Y;
var currentOffset = AssociatedObject.Offset;
AssociatedObject.Offset = new Vector(
currentOffset.X,
currentOffset.Y + deltaY);
_lastTouchPoint = currentPoint;
}
private void OnTouchEnd(object sender, TouchEventArgs e)
{
_isTouching = false;
// Could trigger inertia here
}
protected override void OnDetaching()
{
AssociatedObject.TouchBegin -= OnTouchBegin;
AssociatedObject.TouchMove -= OnTouchMove;
AssociatedObject.TouchEnd -= OnTouchEnd;
base.OnDetaching();
}
}Performance Optimization
Virtualization for Large Content
Combine smooth scrolling with virtualization for optimal performance:
<ScrollViewer behaviors:SmoothScrollBehavior.IsEnabled="True">
<VirtualizingStackPanel>
<!-- Virtualized content for large lists -->
</VirtualizingStackPanel>
</ScrollViewer>Throttling Updates
Prevent excessive rendering during smooth scrolling:
private readonly ThrottleDispatcher _throttle = new ThrottleDispatcher(TimeSpan.FromMilliseconds(16));
private async void StartSmoothScroll()
{
while (_isAnimating)
{
await _throttle.Yield();
// Update scroll position
// ...
}
}GPU Acceleration
Enable GPU acceleration for smoother animations:
<ScrollViewer
x:Name="scrollViewer"
RenderOptions.EdgeMode="Aliased">
<!-- Content with GPU-accelerated rendering -->
</ScrollViewer>Complete Example Application
MainView.axaml
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:YourApp.Behaviors"
Title="Smooth Scrolling Demo"
Width="800"
Height="600">
<Grid RowDefinitions="Auto,*">
<!-- Toolbar -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="10">
<TextBlock Text="Zoom:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Slider
Minimum="0.5"
Maximum="3.0"
Value="{Binding #contentBorder.(behaviors:ZoomBehavior.ZoomFactor)}"
Width="200"/>
<TextBlock
Text="{Binding #contentBorder.(behaviors:ZoomBehavior.ZoomFactor), StringFormat={}{0:F1}x}"
VerticalAlignment="Center"
Margin="10,0,0,0"/>
</StackPanel>
<!-- Scrollable Content -->
<ScrollViewer
Grid.Row="1"
behaviors:SmoothScrollBehavior.IsEnabled="True"
behaviors:InertiaScrollBehavior.IsEnabled="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Border
x:Name="contentBorder"
behaviors:ZoomBehavior.MinZoom="0.5"
behaviors:ZoomBehavior.MaxZoom="3.0"
Background="White">
<StackPanel Margin="20">
<!-- Generate sample content -->
<TextBlock
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,20"
Text="Smooth Scrolling Demo"/>
<TextBlock
TextWrapping="Wrap"
Margin="0,0,0,20"
Text="Use Ctrl + Mouse Wheel to zoom. Scroll normally to see smooth scrolling in action."/>
<!-- Sample content items -->
<ItemsControl ItemsSource="{Binding SampleItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
BorderBrush="Gray"
BorderThickness="1"
Margin="5"
Padding="15"
CornerRadius="5"
Background="#F5F5F5">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</ScrollViewer>
</Grid>
</Window>ViewModel
public class MainViewModel : ViewModelBase
{
public ObservableCollection<string> SampleItems { get; }
public MainViewModel()
{
SampleItems = new ObservableCollection<string>();
// Generate sample content
for (int i = 1; i <= 100; i++)
{
SampleItems.Add($"Item {i}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
}
}
}Troubleshooting Common Issues
Issue 1: Jittery Scrolling
Symptoms: Scrolling appears choppy or stuttering
Solutions:
- Reduce smoothness factor for faster response
- Enable GPU acceleration
- Implement virtualization for large content
- Check for expensive layout operations in content
Issue 2: Zoom Not Centered
Symptoms: Zoom focuses on wrong point
Solution:
AssociatedObject.RenderTransformOrigin = new RelativePoint(
mousePosition.X / controlWidth,
mousePosition.Y / controlHeight,
RelativeUnit.Relative);Issue 3: Touch Gestures Not Working
Symptoms: Touch scrolling doesn't respond
Solutions:
- Ensure touch events are properly subscribed
- Check for event handling conflicts
- Verify touch input is enabled on target platform
Platform-Specific Considerations
Windows
- Leverage Windows inertia APIs for native feel
- Consider high-DPI scaling
- Test with both mouse and touch input
macOS
- Match native trackpad scrolling behavior
- Implement proper momentum scrolling
- Consider system smooth scrolling settings
Linux
- Test across different desktop environments
- Account for varying touch support
- Consider X11 vs Wayland differences
Conclusion
Implementing smooth scrolling and zooming in Avalonia UI significantly enhances user experience, bringing your applications closer to the polished feel users expect from modern software. While Avalonia provides some built-in support, custom implementations offer greater control and consistency across platforms.
Key Takeaways:
- Smooth scrolling improves perceived quality and user comfort
- Inertia-based scrolling creates more natural interaction
- Zoom functionality enhances content exploration
- Touch support is essential for modern applications
- Performance optimization ensures smooth experience on all hardware
By implementing these techniques thoughtfully, you can create Avalonia applications that not only function excellently but feel delightful to use. Remember to test across all target platforms and adjust parameters to match platform conventions while maintaining consistent behavior.
The investment in smooth scrolling and zooming pays dividends in user satisfaction and professional perception—making your applications stand out in an increasingly competitive software landscape.
About This Guide: This comprehensive tutorial covers smooth scrolling and zooming implementation in Avalonia UI, from basic concepts to advanced optimization techniques. Suitable for developers seeking to enhance their Avalonia applications with polished, professional user interactions.