Avalonia’s composition system provides a low-level API for creating high-performance visual effects and animations. It separates the UI thread from the render thread, enabling smooth animations even when the UI thread is busy.
Composition Architecture
The composition system consists of two main components:
- UI Thread (Compositor) - Manages composition objects and batches changes
- Render Thread (ServerCompositor) - Processes batches and renders visuals
From ~/workspace/source/src/Avalonia.Base/Rendering/Composition/Compositor.cs:
public partial class Compositor
{
internal IRenderLoop Loop { get; }
private readonly ServerCompositor _server;
private CompositionBatch? _nextCommit;
private readonly BatchStreamObjectPool<object?> _batchObjectPool;
private readonly BatchStreamMemoryPool _batchMemoryPool;
public Task RequestCommitAsync() =>
RequestCompositionBatchCommitAsync().Processed;
}
Getting Started with Composition
Accessing the Compositor
using Avalonia.Rendering.Composition;
using Avalonia.Media;
public class MyControl : Control
{
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
if (compositor != null)
{
SetupComposition(compositor);
}
}
private void SetupComposition(Compositor compositor)
{
var visual = compositor.CreateContainerVisual();
visual.Size = new Vector(200, 200);
visual.Offset = new Vector3D(50, 50, 0);
ElementComposition.SetElementChildVisual(this, visual);
}
}
Composition Objects
Visual Types
From ~/workspace/source/src/Avalonia.Base/Rendering/Composition/:
ContainerVisual
public class ContainerVisual : Visual
{
public VisualCollection Children { get; }
}
A visual that can contain child visuals.
CompositionDrawListVisual
public class CompositionDrawListVisual : Visual
{
// Renders using a pre-recorded draw list
}
Optimized for static or infrequently changing content.
CompositionCustomVisual
public class CompositionCustomVisual : Visual
{
// Custom rendering via handler
}
Allows custom rendering logic.
Visual Properties
var visual = compositor.CreateContainerVisual();
// Transform properties
visual.Offset = new Vector3D(x, y, z);
visual.Size = new Vector(width, height);
visual.AnchorPoint = new Vector3D(0.5, 0.5, 0); // Center
visual.RotationAngle = Math.PI / 4; // 45 degrees
visual.Scale = new Vector3D(2, 2, 1); // 2x scale
visual.TransformMatrix = Matrix.CreateRotation(angle);
// Appearance properties
visual.Opacity = 0.5;
visual.IsVisible = true;
visual.ClipToBounds = true;
// Advanced properties
visual.OpacityMask = compositor.CreateGradientBrush(...);
visual.Effect = compositor.CreateBlurEffect(radius: 10);
Composition Animations
Composition animations run on the render thread, independent of the UI thread.
Creating Animations
using Avalonia.Rendering.Composition.Animations;
private void AnimateVisual(Compositor compositor, Visual visual)
{
// Create animation
var animation = compositor.CreateVector3DKeyFrameAnimation();
// Define keyframes
animation.InsertKeyFrame(0.0f, new Vector3D(0, 0, 0));
animation.InsertKeyFrame(0.5f, new Vector3D(100, 0, 0));
animation.InsertKeyFrame(1.0f, new Vector3D(100, 100, 0));
// Configure animation
animation.Duration = TimeSpan.FromSeconds(2);
animation.IterationBehavior = AnimationIterationBehavior.Forever;
animation.Direction = AnimationDirection.Alternate;
// Start animation
visual.StartAnimation("Offset", animation);
}
Animation Types
Keyframe Animations
// Scalar (double) animation
var scalarAnimation = compositor.CreateScalarKeyFrameAnimation();
scalarAnimation.InsertKeyFrame(0.0f, 0.0);
scalarAnimation.InsertKeyFrame(1.0f, 360.0);
visual.StartAnimation("RotationAngle", scalarAnimation);
// Vector2D animation
var vector2Animation = compositor.CreateVector2DKeyFrameAnimation();
vector2Animation.InsertKeyFrame(0.0f, new Vector(100, 100));
vector2Animation.InsertKeyFrame(1.0f, new Vector(200, 200));
visual.StartAnimation("Size", vector2Animation);
// Color animation
var colorAnimation = compositor.CreateColorKeyFrameAnimation();
colorAnimation.InsertKeyFrame(0.0f, Colors.Red);
colorAnimation.InsertKeyFrame(1.0f, Colors.Blue);
solidColorBrush.StartAnimation("Color", colorAnimation);
Easing Functions
animation.InsertKeyFrame(1.0f, targetValue,
compositor.CreateCubicBezierEasingFunction(
new Vector(0.42, 0.0), // Control point 1
new Vector(0.58, 1.0) // Control point 2
));
// Or use built-in easings
animation.InsertKeyFrame(1.0f, targetValue,
compositor.CreateLinearEasingFunction());
Expression Animations
Expression animations compute values based on properties of other objects:
var expression = compositor.CreateExpressionAnimation(
"visual1.Offset.X + 100");
expression.SetReferenceParameter("visual1", otherVisual);
visual.StartAnimation("Offset.X", expression);
Common expressions:
// Follow another visual with offset
"target.Offset + Vector3D(50, 0, 0)"
// Scale based on parent size
"parent.Size.X / 2"
// Animate based on time
"Sin(Time.TotalSeconds * 2 * Pi) * 100"
// Conditional animation
"visual.Opacity > 0.5 ? 1.0 : 0.0"
Composition Brushes
Solid Color Brush
var brush = compositor.CreateColorBrush(Colors.Blue);
visual.OpacityMask = brush;
// Animate brush color
var colorAnim = compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(1.0f, Colors.Red);
colorAnim.Duration = TimeSpan.FromSeconds(1);
brush.StartAnimation("Color", colorAnim);
Gradient Brushes
var gradientBrush = compositor.CreateLinearGradientBrush();
gradientBrush.StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative);
gradientBrush.EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative);
gradientBrush.GradientStops.Add(
compositor.CreateColorGradientStop(0.0f, Colors.Blue));
gradientBrush.GradientStops.Add(
compositor.CreateColorGradientStop(1.0f, Colors.Green));
Custom Composition Visuals
Creating Custom Rendering
public class CustomCompositionHandler : CompositionCustomVisualHandler
{
public override void OnMessage(object message)
{
// Handle messages from UI thread
}
public override void OnRender(ICompositionDrawingSurface surface)
{
using (var context = surface.CreateDrawingContext())
{
// Custom rendering code
context.DrawRectangle(
Brushes.Red,
null,
new Rect(0, 0, 100, 100));
}
}
public override void OnAnimationFrameUpdate()
{
// Called on every frame when animation is running
Invalidate();
}
}
// Usage
var customVisual = compositor.CreateCustomVisual(new CustomCompositionHandler());
customVisual.Size = new Vector(200, 200);
ElementComposition.SetElementChildVisual(control, customVisual);
// Send message to handler
customVisual.SendHandlerMessage(new { Type = "Update", Data = 42 });
Composition Effects
Blur Effect
var blurEffect = compositor.CreateBlurEffect();
blurEffect.BlurRadius = 10;
blurEffect.Source = compositor.CreateVisualSurface(sourceVisual);
var effectBrush = compositor.CreateEffectBrush(blurEffect);
visual.OpacityMask = effectBrush;
// Animate blur radius
var blurAnimation = compositor.CreateScalarKeyFrameAnimation();
blurAnimation.InsertKeyFrame(0.0f, 0);
blurAnimation.InsertKeyFrame(1.0f, 20);
blurAnimation.Duration = TimeSpan.FromSeconds(1);
blurEffect.StartAnimation("BlurRadius", blurAnimation);
Batch Commits
Changes to composition objects are batched and committed atomically.
Manual Commit
private async Task UpdateCompositionAsync(Compositor compositor)
{
// Make multiple changes
visual1.Offset = new Vector3D(100, 100, 0);
visual2.Opacity = 0.5;
visual3.Size = new Vector(200, 200);
// Request commit and wait for processing
await compositor.RequestCommitAsync();
// Changes are now visible on render thread
}
Automatic Commit
By default, changes are committed automatically at the end of each render loop iteration.
Property Sets
Use property sets to avoid creating multiple animations:
var properties = compositor.CreateAnimationPropertySet();
properties.InsertScalar("Progress", 0);
// Animate the property set
var animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 1.0);
animation.Duration = TimeSpan.FromSeconds(2);
properties.StartAnimation("Progress", animation);
// Reference in expressions
var expr1 = compositor.CreateExpressionAnimation(
"props.Progress * 100");
expr1.SetReferenceParameter("props", properties);
visual1.StartAnimation("Offset.X", expr1);
var expr2 = compositor.CreateExpressionAnimation(
"props.Progress * 200");
expr2.SetReferenceParameter("props", properties);
visual2.StartAnimation("Offset.X", expr2);
Visual Surfaces
Cache rendered content using visual surfaces:
var surface = compositor.CreateVisualSurface();
surface.SourceVisual = complexVisual;
surface.SourceSize = new Vector(width, height);
// Use surface as texture
var surfaceBrush = compositor.CreateSurfaceBrush(surface);
cachedVisual.OpacityMask = surfaceBrush;
Use visual surfaces to cache expensive rendering operations:
- Complex vector graphics
- Text rendering
- Multiple layers with effects
- Content that doesn’t change frequently
Composition Options
var options = new CompositionOptions
{
// Enable/disable specific features
UseRegionTracking = true,
// Memory management
MaxResourceCacheSize = 128 * 1024 * 1024, // 128 MB
};
AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(options)
.StartWithClassicDesktopLifetime(args);
Advanced Scenarios
Synchronized Animations
public class SynchronizedAnimations
{
public void CreateSynchronizedFade(
Compositor compositor,
Visual visual1,
Visual visual2)
{
var animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0.0f, 1.0);
animation.InsertKeyFrame(1.0f, 0.0);
animation.Duration = TimeSpan.FromSeconds(1);
// Both visuals share the same animation timeline
visual1.StartAnimation("Opacity", animation);
visual2.StartAnimation("Opacity", animation);
}
}
Implicit Animations
Animations that start automatically when a property changes:
var implicitAnimation = compositor.CreateVector3DKeyFrameAnimation();
implicitAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
implicitAnimation.Duration = TimeSpan.FromMilliseconds(300);
implicitAnimation.Target = "Offset";
var implicitAnimations = compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = implicitAnimation;
visual.ImplicitAnimations = implicitAnimations;
// Now any change to Offset will automatically animate
visual.Offset = new Vector3D(100, 100, 0); // Animates smoothly
Interaction Animations
Create animations that respond to user input:
public class InteractionAnimation
{
private Visual _visual;
private Compositor _compositor;
public void SetupDragAnimation()
{
// Create expression that follows pointer position
var expression = _compositor.CreateExpressionAnimation(
"Vector3D(pointer.Position.X, pointer.Position.Y, 0)");
// Start during pointer press
_visual.StartAnimation("Offset", expression);
}
}
Best Practices
Thread Safety: Composition objects are not thread-safe. Always access them from the UI thread, except during custom visual rendering.
When to Use Composition
Use composition for:
- Smooth animations independent of UI thread
- Complex visual effects
- High-performance scrolling scenarios
- Custom rendering that needs to run at 60+ FPS
- Interactive animations responding to input
Use standard controls for:
- Simple UI layouts
- Standard control animations
- Data binding scenarios
- Form-based applications
Memory Management
// Dispose visuals when no longer needed
private void CleanupComposition()
{
_visual?.Dispose();
_animation?.Dispose();
_brush?.Dispose();
}
protected override void OnDetachedFromVisualTree(
VisualTreeAttachmentEventArgs e)
{
CleanupComposition();
base.OnDetachedFromVisualTree(e);
}
Debugging Composition
Enable Composition Debugging
// Set environment variable before app starts
Environment.SetEnvironmentVariable(
"AVALONIA_COMPOSITION_DEBUG", "1");
// Or enable specific diagnostics
var compositor = ElementComposition.GetElementVisual(this)?.Compositor;
if (compositor?.Server is ICompositionTargetDebugEvents debug)
{
debug.RenderComplete += (s, e) =>
Console.WriteLine($"Frame rendered in {e.RenderTime}ms");
}
See Also