Building a Dynamic Milestone Journey Tracker with SkiaSharp in .NET MAUI


π Introduction
A visual progress tracker elevates user experience. In this post, weβll create a milestone journey tracker in .NET MAUI that animates a curved path and displays milestone images dynamically along it, updating completed ones with a tick image.
π§° Tools & Technologies
.NET MAUI
SkiaSharp
AbsoluteLayout
Custom Animation
Dynamic Image Placement
πΌοΈ UI Layout in XAML
We define a Border
with an AbsoluteLayout
that hosts the animated path and milestone images.
<Grid RowDefinitions="*,*">
<Border Padding="10" Stroke="#1A299494" StrokeThickness="1" StrokeShape="RoundRectangle 7">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#1A299494" Offset="0.0" />
<GradientStop Color="#fbfcfc" Offset="1.0" />
</LinearGradientBrush>
</Border.Background>
<AbsoluteLayout x:Name="RootLayout">
<skia:SKCanvasView x:Name="RoadCanvas"
PaintSurface="OnPaintSurface"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All" />
</AbsoluteLayout>
</Border>
</Grid>
π― Key Variables Used
Before diving into SkiaSharp drawing logic, here are the core variables that manage animation, path points, and milestones dynamically:
// Stores the calculated path points to align milestones along the curved path
private List<(float x, float y)> pathPoints = new();
// Represents each milestone's state and associated image
private List<Milestone> milestones = new();
// Stopwatch to control path animation timing
Stopwatch pathAnimationTimer = new();
// Progress of the animation (0 to 1)
float animationProgress = 0f;
// Duration of the curved path animation in milliseconds
const int AnimationDurationMs = 800;
These variables drive the animation logic and help dynamically position milestone images smoothly on the path.
π¨ Drawing the Curved Animated Path with Animation
Here's how we draw the curved path using sine waves and animate its drawing:
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear();
int width = e.Info.Width;
int height = e.Info.Height;
if (!pathAnimationTimer.IsRunning)
{
pathAnimationTimer.Start();
}
float elapsed = pathAnimationTimer.ElapsedMilliseconds;
animationProgress = Math.Min(elapsed / AnimationDurationMs, 1f);
using var pathPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 6,
Color = new SKColor(15, 167, 152), //
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
var path = new SKPath();
path.MoveTo(0, height / 2f);
pathPoints.Clear(); // Prevent duplication or out-of-sync points
for (float x = 0; x <= width * animationProgress; x += 10)
{
float y = (float)(height / 2 + Math.Sin(x * 0.01) * 50);
path.LineTo(x, y);
pathPoints.Add((x, y)); // Store for GetClosestPathPoint
}
canvas.DrawPath(path, pathPaint);
if (animationProgress < 1)
{
((SKCanvasView)sender).InvalidateSurface(); // Keep animating
}
else
{
PlaceMilestoneImages(width, height); // Only after path fully drawn
}
}
πΌοΈ Displaying Milestone Images on the Path
You dynamically place images at equal distances along the curve using points from pathPoints
.
private async void PlaceMilestoneImages(int canvasWidth, int canvasHeight)
{
// Remove previous images
for (int i = RootLayout.Children.Count - 1; i >= 0; i--)
{
if (RootLayout.Children[i] is Image)
RootLayout.Children.RemoveAt(i);
}
float dipPadding = 25f; // Padding you have added around the canvas
float density = (float)DeviceDisplay.MainDisplayInfo.Density;
float pixelPadding = dipPadding * density;
float drawableWidth = canvasWidth - 2 * pixelPadding;
for (int i = 0; i < milestones.Count; i++)
{
float progress = (float)i / (milestones.Count - 1);
float x = pixelPadding + drawableWidth * progress;
var closest = GetClosestPathPoint(x);
float y = closest.y;
// Convert pixels to DIP
double dipX = closest.x / density;
double dipY = closest.y / density;
var milestone = milestones[i];
var image = new Image
{
Source = milestone.IsCompleted ? "dash_check" : milestone.ImageSource,
WidthRequest = 35,
HeightRequest = 35,
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center,
Opacity = 0,
Scale = 0.5
};
AbsoluteLayout.SetLayoutBounds(image, new Rect(dipX - 16, dipY - 16, 32, 32));
AbsoluteLayout.SetLayoutFlags(image, AbsoluteLayoutFlags.None);
RootLayout.Children.Add(image);
// Animate
await Task.Delay(100); // Stagger for effect
_ = image.FadeTo(1, 300, Easing.CubicIn);
_ = image.ScaleTo(1, 300, Easing.BounceOut);
}
await Task.Delay(100); // Stagger for effect
_ = borProgress.FadeTo(1, 300, Easing.CubicIn);
_ = borProgress.ScaleTo(1, 300, Easing.BounceOut);
}
π Helper Method: Get Closest Path Point
private (float x, float y) GetClosestPathPoint(float targetX)
{
(float x, float y) closest = pathPoints[0];
float minDist = Math.Abs(closest.x - targetX);
foreach (var point in pathPoints)
{
float dist = Math.Abs(point.x - targetX);
if (dist < minDist)
{
minDist = dist;
closest = point;
}
}
return closest;
}
π§ͺ Pro Tips
Cache milestone list if coming from API
Always remove previous images to avoid stacking
Add a
ResetTracker()
method to reanimate from startConsider SVGs or Lottie for more complex visuals
π Conclusion
You now have a visually rich, animated Milestone Tracker for any onboarding or progress-based feature in your .NET MAUI app.
Subscribe to my newsletter
Read articles from Ali Raza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ali Raza
Ali Raza
π Tech Lead | .NET MAUI Expert | Mobile App Developer I'm Ali Raza, a passionate Tech Lead with over 6 years of experience in mobile app development. I specialize in .NET MAUI/Xamarin and have led multiple high-impact projects, including enterprise apps, fintech solutions, and eSIM technology. πΉ What I Do: β .NET MAUI & Xamarin β Building cross-platform apps with robust architectures β eSIM & Fintech Apps β Leading innovations in digital connectivity & finance β Performance Optimization β Creating high-quality, scalable mobile solutions β Mentorship & Community Sharing β Helping developers master .NET MAUI π’ Sharing Weekly Insights on .NET MAUI/Xamarin to help developers level up! Follow me for deep dives into Native Interop, API Optimization, and Advanced UI/UX techniques. Letβs connect and build something amazing! π