Drawing with Microsoft.Maui.Graphics: Custom UI Without SkiaSharp
MAUI ships with a built-in cross-platform drawing API: Microsoft.Maui.Graphics. You don't need SkiaSharp for custom visuals — the framework provides shapes, paths, gradients, and text rendering out of the box. It's not as feature-rich as SkiaSharp, but for most custom UI work, it's more than sufficient.
GraphicsView and IDrawable
The drawing surface in MAUI is GraphicsView, which takes an IDrawable implementation:
<GraphicsView Drawable="{StaticResource MyDrawable}"
HeightRequest="300"
WidthRequest="300" />
public class MyDrawable : IDrawable
{
public void Draw(ICanvas canvas, RectF dirtyRect)
{
// All drawing happens here
}
}
Register it as a resource or bind it from your view model.
Basic Shapes
The ICanvas API is straightforward:
public class ShapesDrawable : IDrawable
{
public void Draw(ICanvas canvas, RectF dirtyRect)
{
// Filled rectangle
canvas.FillColor = Colors.SteelBlue;
canvas.FillRectangle(10, 10, 100, 60);
// Stroked rectangle
canvas.StrokeColor = Colors.DarkSlateGray;
canvas.StrokeSize = 2;
canvas.DrawRectangle(10, 80, 100, 60);
// Circle
canvas.FillColor = Colors.Coral;
canvas.FillCircle(170, 40, 30);
// Rounded rectangle
canvas.FillColor = Colors.MediumSeaGreen;
canvas.FillRoundedRectangle(130, 80, 100, 60, 12);
}
}
Paths for Complex Shapes
For anything beyond basic shapes, build paths:
public class ArrowDrawable : IDrawable
{
public void Draw(ICanvas canvas, RectF dirtyRect)
{
var path = new PathF();
path.MoveTo(50, 10);
path.LineTo(90, 50);
path.LineTo(70, 50);
path.LineTo(70, 90);
path.LineTo(30, 90);
path.LineTo(30, 50);
path.LineTo(10, 50);
path.Close();
canvas.FillColor = Colors.OrangeRed;
canvas.FillPath(path);
canvas.StrokeColor = Colors.DarkRed;
canvas.StrokeSize = 2;
canvas.DrawPath(path);
}
}
Gradients
Apply linear and radial gradients:
public void Draw(ICanvas canvas, RectF dirtyRect)
{
var linearGradient = new LinearGradientPaint(
Colors.Navy, Colors.LightSkyBlue,
new Point(0, 0), new Point(1, 1));
canvas.SetFillPaint(linearGradient, dirtyRect);
canvas.FillRoundedRectangle(10, 10, dirtyRect.Width - 20,
dirtyRect.Height - 20, 16);
// Radial gradient
var radialGradient = new RadialGradientPaint(
new[] {
new PaintGradientStop(0f, Colors.White),
new PaintGradientStop(1f, Colors.Transparent)
},
new Point(0.5, 0.5), 0.5);
canvas.SetFillPaint(radialGradient, new RectF(50, 50, 100, 100));
canvas.FillCircle(100, 100, 50);
}
Drawing Text
Render text with font control:
public void Draw(ICanvas canvas, RectF dirtyRect)
{
canvas.FontColor = Colors.White;
canvas.FontSize = 24;
canvas.Font = Microsoft.Maui.Graphics.Font.Default;
canvas.DrawString("Hello, MAUI Graphics!",
dirtyRect.X, dirtyRect.Y,
dirtyRect.Width, dirtyRect.Height,
HorizontalAlignment.Center,
VerticalAlignment.Center);
}
A Practical Example: Simple Bar Chart
Here's something you'd actually use — a basic bar chart:
public class BarChartDrawable : IDrawable
{
public List<ChartEntry> Entries { get; set; } = new();
public void Draw(ICanvas canvas, RectF dirtyRect)
{
if (Entries.Count == 0) return;
var padding = 20f;
var chartWidth = dirtyRect.Width - padding * 2;
var chartHeight = dirtyRect.Height - padding * 2;
var barWidth = chartWidth / Entries.Count * 0.7f;
var gap = chartWidth / Entries.Count * 0.3f;
var maxValue = Entries.Max(e => e.Value);
// Draw baseline
canvas.StrokeColor = Colors.Gray;
canvas.StrokeSize = 1;
canvas.DrawLine(padding, dirtyRect.Height - padding,
dirtyRect.Width - padding, dirtyRect.Height - padding);
for (int i = 0; i < Entries.Count; i++)
{
var entry = Entries[i];
var barHeight = (entry.Value / maxValue) * chartHeight;
var x = padding + i * (barWidth + gap);
var y = dirtyRect.Height - padding - barHeight;
// Bar
canvas.FillColor = entry.Colour;
canvas.FillRoundedRectangle(x, y, barWidth, barHeight, 4);
// Label
canvas.FontColor = Colors.Gray;
canvas.FontSize = 12;
canvas.DrawString(entry.Label,
x, dirtyRect.Height - padding + 5,
barWidth, 20,
HorizontalAlignment.Center,
VerticalAlignment.Top);
}
}
}
public record ChartEntry(string Label, float Value, Color Colour);
Use it in XAML:
<GraphicsView HeightRequest="250">
<GraphicsView.Drawable>
<local:BarChartDrawable x:Name="chart" />
</GraphicsView.Drawable>
</GraphicsView>
Invalidation and Redrawing
GraphicsView doesn't redraw automatically when data changes. Call Invalidate() to trigger a redraw:
// In your page code-behind or via a binding
chart.Entries = newEntries;
graphicsView.Invalidate();
For animated or frequently updated graphics, you can set up a timer:
var timer = Application.Current.Dispatcher.CreateTimer();
timer.Interval = TimeSpan.FromMilliseconds(16); // ~60fps
timer.Tick += (s, e) => graphicsView.Invalidate();
timer.Start();
Interaction
GraphicsView supports touch interaction through events:
<GraphicsView Drawable="{StaticResource MyDrawable}"
StartInteraction="OnStartInteraction"
MoveInteraction="OnMoveInteraction"
EndInteraction="OnEndInteraction" />
private void OnStartInteraction(object sender,
TouchEventArgs e)
{
var point = e.Touches.FirstOrDefault();
// Hit test against your drawn elements
}
When to Use Graphics vs SkiaSharp
Use Maui.Graphics for simple charts, custom progress indicators, decorative elements, and lightweight drawings. It's built in, requires no additional packages, and works across all platforms.
Use SkiaSharp when you need advanced features: complex path effects, shader-based rendering, image manipulation, PDF generation, or high-performance animation.
For most business applications, the built-in graphics API is more than enough to create polished custom visuals.