.NET MAUI Graphics Drawing
Common gotchas
| Issue |
Fix |
| Nothing draws on screen |
Ensure Drawable is set on GraphicsView and the control has non-zero HeightRequest/WidthRequest |
| State bleeds between shapes |
Wrap isolated sections in SaveState() / RestoreState() pairs |
| Shadows stick to later draws |
Call canvas.SetShadow(SizeF.Zero, 0, null) after drawing the shadowed element |
| Clipping never resets |
Clipping is cumulative per frame — use SaveState/RestoreState around clip regions |
| UI freezes during drawing |
Never do I/O, network, or heavy computation inside Draw() — it runs on the UI thread |
Canvas state — always pair Save/Restore
⚠️ Unpaired SaveState/RestoreState causes state leaks across draw calls.
// ✅ Correct — isolated state
canvas.SaveState();
canvas.StrokeColor = Colors.Red;
canvas.StrokeSize = 6;
canvas.DrawRectangle(10, 10, 80, 80);
canvas.RestoreState();
// Stroke reverts to previous values
// ❌ Wrong — state leaks to everything drawn after
canvas.StrokeColor = Colors.Red;
canvas.StrokeSize = 6;
canvas.DrawRectangle(10, 10, 80, 80);
// Every subsequent shape is now red with size 6
Saves/restores: stroke, fill, font, shadow, clip, and transforms. Nest calls for layered isolation.
Triggering redraws
// ✅ Correct — queue a redraw
graphicsView.Invalidate();
// ❌ Wrong — never call Draw() directly
myDrawable.Draw(canvas, rect);
Invalidate() queues a redraw; the framework calls IDrawable.Draw on the next frame.
- ⚠️ Avoid calling
Invalidate() in a tight loop — batch state changes, then invalidate once.
Performance tips
- Keep
Draw() fast — pre-compute paths and data outside the draw method; Draw() is called on every frame.
- Reuse
PathF objects — create them once, store as fields, draw repeatedly.
- Use
MeasureFirstItem-style thinking — if drawing many identical items, calculate dimensions once.
- Minimize allocations — avoid
new PathF() inside Draw() when the path doesn't change.
// ✅ Pre-computed path (field)
private readonly PathF _starPath = BuildStarPath();
public void Draw(ICanvas canvas, RectF dirtyRect)
{
canvas.FillPath(_starPath);
}
// ❌ Allocating every frame
public void Draw(ICanvas canvas, RectF dirtyRect)
{
var path = new PathF();
// ...build path every frame...
canvas.FillPath(path);
}
Shadows and clipping — sticky state pitfalls
Shadows apply to all subsequent draws until explicitly removed. Clips accumulate and can only be undone with RestoreState():
// ✅ Shadow: remove after use
canvas.SetShadow(new SizeF(5, 5), 4, Colors.Gray);
canvas.FillRectangle(20, 20, 100, 60);
canvas.SetShadow(SizeF.Zero, 0, null); // ← must remove
// ✅ Clip: isolate with SaveState/RestoreState
canvas.SaveState();
canvas.ClipRectangle(20, 20, 100, 100);
canvas.FillRectangle(0, 0, 200, 200); // clipped
canvas.RestoreState(); // clip removed
// ❌ Clip persists — everything after is also clipped
canvas.ClipRectangle(20, 20, 100, 100);
canvas.FillEllipse(150, 150, 50, 50); // unintentionally clipped!
Set properties BEFORE draw calls
// ✅ Properties then draw
canvas.StrokeColor = Colors.Blue;
canvas.DrawRectangle(10, 10, 100, 50);
// ❌ Setting after draw has no effect on previous shape
canvas.DrawRectangle(10, 10, 100, 50);
canvas.StrokeColor = Colors.Blue;
Decision framework
| Need |
Approach |
| Simple shapes / static graphics |
Single IDrawable, draw in Draw() |
| Animated graphics |
Update state externally, call Invalidate() from timer/animation |
| Complex layered scene |
Multiple SaveState/RestoreState blocks, or separate drawables |
| Hit testing on drawn elements |
Track shape bounds manually — GraphicsView has no built-in hit test on drawn content |
| Platform-specific rendering |
Use handlers/platform code; Microsoft.Maui.Graphics is cross-platform only |
Quick checklist
1---2name: maui-graphics-drawing3description: >4 Guidance for custom drawing with Microsoft.Maui.Graphics, GraphicsView, canvas drawing operations,5 shapes, paths, text rendering, image drawing, shadows, clipping, and canvas state management.6 USE FOR: "custom drawing", "GraphicsView", "canvas drawing", "draw shapes", "draw path",7 "draw text", "ICanvas", "IDrawable", "shadows", "clipping", "Microsoft.Maui.Graphics".8 DO NOT USE FOR: view animations (use maui-animations), gesture handling on drawn elements9 (use maui-gestures), or app icons (use maui-app-icons-splash).10---1112# .NET MAUI Graphics Drawing1314## Common gotchas1516| Issue | Fix |17|---|---|18| Nothing draws on screen | Ensure `Drawable` is set on `GraphicsView` and the control has non-zero `HeightRequest`/`WidthRequest` |19| State bleeds between shapes | Wrap isolated sections in `SaveState()` / `RestoreState()` pairs |20| Shadows stick to later draws | Call `canvas.SetShadow(SizeF.Zero, 0, null)` after drawing the shadowed element |21| Clipping never resets | Clipping is cumulative per frame — use `SaveState`/`RestoreState` around clip regions |22| UI freezes during drawing | Never do I/O, network, or heavy computation inside `Draw()` — it runs on the UI thread |2324## Canvas state — always pair Save/Restore2526⚠️ Unpaired `SaveState`/`RestoreState` causes state leaks across draw calls.2728```csharp29// ✅ Correct — isolated state30canvas.SaveState();31canvas.StrokeColor = Colors.Red;32canvas.StrokeSize = 6;33canvas.DrawRectangle(10, 10, 80, 80);34canvas.RestoreState();35// Stroke reverts to previous values3637// ❌ Wrong — state leaks to everything drawn after38canvas.StrokeColor = Colors.Red;39canvas.StrokeSize = 6;40canvas.DrawRectangle(10, 10, 80, 80);41// Every subsequent shape is now red with size 642```4344Saves/restores: stroke, fill, font, shadow, clip, and transforms. Nest calls for layered isolation.4546## Triggering redraws4748```csharp49// ✅ Correct — queue a redraw50graphicsView.Invalidate();5152// ❌ Wrong — never call Draw() directly53myDrawable.Draw(canvas, rect);54```5556- `Invalidate()` queues a redraw; the framework calls `IDrawable.Draw` on the next frame.57- ⚠️ Avoid calling `Invalidate()` in a tight loop — batch state changes, then invalidate once.5859## Performance tips6061- **Keep `Draw()` fast** — pre-compute paths and data outside the draw method; `Draw()` is called on every frame.62- **Reuse `PathF` objects** — create them once, store as fields, draw repeatedly.63- **Use `MeasureFirstItem`-style thinking** — if drawing many identical items, calculate dimensions once.64- **Minimize allocations** — avoid `new PathF()` inside `Draw()` when the path doesn't change.6566```csharp67// ✅ Pre-computed path (field)68private readonly PathF _starPath = BuildStarPath();6970public void Draw(ICanvas canvas, RectF dirtyRect)71{72 canvas.FillPath(_starPath);73}7475// ❌ Allocating every frame76public void Draw(ICanvas canvas, RectF dirtyRect)77{78 var path = new PathF();79 // ...build path every frame...80 canvas.FillPath(path);81}82```8384## Shadows and clipping — sticky state pitfalls8586Shadows apply to **all subsequent draws** until explicitly removed. Clips accumulate and can only be undone with `RestoreState()`:8788```csharp89// ✅ Shadow: remove after use90canvas.SetShadow(new SizeF(5, 5), 4, Colors.Gray);91canvas.FillRectangle(20, 20, 100, 60);92canvas.SetShadow(SizeF.Zero, 0, null); // ← must remove9394// ✅ Clip: isolate with SaveState/RestoreState95canvas.SaveState();96canvas.ClipRectangle(20, 20, 100, 100);97canvas.FillRectangle(0, 0, 200, 200); // clipped98canvas.RestoreState(); // clip removed99100// ❌ Clip persists — everything after is also clipped101canvas.ClipRectangle(20, 20, 100, 100);102canvas.FillEllipse(150, 150, 50, 50); // unintentionally clipped!103```104105## Set properties BEFORE draw calls106107```csharp108// ✅ Properties then draw109canvas.StrokeColor = Colors.Blue;110canvas.DrawRectangle(10, 10, 100, 50);111112// ❌ Setting after draw has no effect on previous shape113canvas.DrawRectangle(10, 10, 100, 50);114canvas.StrokeColor = Colors.Blue;115```116117## Decision framework118119| Need | Approach |120|---|---|121| Simple shapes / static graphics | Single `IDrawable`, draw in `Draw()` |122| Animated graphics | Update state externally, call `Invalidate()` from timer/animation |123| Complex layered scene | Multiple `SaveState`/`RestoreState` blocks, or separate drawables |124| Hit testing on drawn elements | Track shape bounds manually — `GraphicsView` has no built-in hit test on drawn content |125| Platform-specific rendering | Use handlers/platform code; `Microsoft.Maui.Graphics` is cross-platform only |126127## Quick checklist128129- [ ] `GraphicsView` has `Drawable` set and non-zero size130- [ ] `Draw()` is fast — no I/O, no heavy allocations131- [ ] Every `SaveState()` has a matching `RestoreState()`132- [ ] Shadows removed with `SetShadow(SizeF.Zero, 0, null)` after use133- [ ] Clips wrapped in `SaveState`/`RestoreState` blocks134- [ ] Properties set **before** the draw call they apply to135- [ ] `Invalidate()` used instead of calling `Draw()` directly