flutter_svg is the right answer when you need to show a lot of SVG files. But for a single icon you want to animate, recolour per path, clip with, or ship with zero dependencies, a plain CustomPainter is often nicer. The good news: SVG path data maps almost one to one onto Flutter's Path API.
The command table
| SVG | Flutter Path
|
|---|---|
M x y |
moveTo(x, y) |
L x y |
lineTo(x, y) |
H x / V y
|
lineTo with the other coordinate unchanged |
C x1 y1 x2 y2 x y |
cubicTo(x1, y1, x2, y2, x, y) |
Q x1 y1 x y |
quadraticBezierTo(x1, y1, x, y) |
A rx ry rot large sweep x y |
arcToPoint(Offset(x, y), radius: Radius.elliptical(rx, ry), rotation: rot, largeArc: large == 1, clockwise: sweep == 1) |
Z |
close() |
Lowercase (relative) commands have relative… versions (relativeLineTo, relativeCubicTo, relativeArcToPoint and so on), or you can add the current point yourself.
Two commands have no direct method:
-
SandTare "smooth" curves. Their first control point is the previous curve's last control point mirrored around the current point:x1 = 2 * currentX - previousX2,y1 = 2 * currentY - previousY2. If the previous command was not a curve of the same kind, the control point is the current point. -
Alooks scary, but Flutter'sarcToPointtakes exactly SVG's endpoint parameters. The only translation is the flags:large-arc-flag→largeArc,sweep-flag1 →clockwise: true.rotationis in degrees, like SVG. A radius of 0 means a straight line in SVG, so draw alineToinstead.
Scaling the viewBox
SVG coordinates live in the viewBox. To fit them into whatever size the widget gets, scale uniformly and centre, which is what browsers do by default (xMidYMid meet):
final scale = math.min(size.width / 48, size.height / 48);
canvas.translate((size.width - 48 * scale) / 2, (size.height - 48 * scale) / 2);
canvas.scale(scale);
If the viewBox does not start at 0 0, add canvas.translate(-minX, -minY) after the scale.
A full example
This icon:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<rect x="4" y="4" width="40" height="40" rx="8" fill="#1E88E5"/>
<path d="M14 25L21 32L35 17" stroke="white" stroke-width="4"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
becomes:
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CheckIconPainter extends CustomPainter {
const CheckIconPainter();
@override
void paint(Canvas canvas, Size size) {
final scale = math.min(size.width / 48, size.height / 48);
canvas.translate((size.width - 48 * scale) / 2, (size.height - 48 * scale) / 2);
canvas.scale(scale);
// rect with rx="8": straight edges joined by arcs
final path0 = Path()
..moveTo(12, 4)
..lineTo(36, 4)
..arcToPoint(const Offset(44, 12), radius: const Radius.circular(8))
..lineTo(44, 36)
..arcToPoint(const Offset(36, 44), radius: const Radius.circular(8))
..lineTo(12, 44)
..arcToPoint(const Offset(4, 36), radius: const Radius.circular(8))
..lineTo(4, 12)
..arcToPoint(const Offset(12, 4), radius: const Radius.circular(8))
..close();
canvas.drawPath(path0, Paint()..color = const Color(0xFF1E88E5));
final path1 = Path()
..moveTo(14, 25)
..lineTo(21, 32)
..lineTo(35, 17);
canvas.drawPath(
path1,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = const Color(0xFFFFFFFF)
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Use it with CustomPaint(size: const Size(48, 48), painter: const CheckIconPainter()).
Gotchas worth knowing
-
Default fill is black. An SVG shape with no
fillattribute is filled black, andstrokedefaults to none.fill="none"on the root is inherited by every child. -
<line>is never filled, even if a fill is inherited. Only its stroke draws. -
SVG colours are alpha-last, Flutter's are alpha-first.
#1E88E580in SVG isColor(0x801E88E5)in Flutter. - Group opacity applies to the group as a whole in SVG. Multiplying it into each child's colour is close, but overlapping children inside a translucent group will look slightly different.
-
Text, gradients and
<use>need work first. Convert text to paths in your editor, and expand clones.
How I checked the mapping
I wrote a small converter and tested it the boring way: twelve SVGs covering every path command, all four arc flag combinations, rotated and too-small arc radii, basic shapes, nested transforms and real Figma and Inkscape exports were drawn by the generated painters in flutter test (via PictureRecorder → toImage) and compared pixel by pixel with Chrome's rendering of the same files. Differences stayed under 0.6% of pixels, all on anti-aliased edges. Flipping a single arc flag on purpose produced 6.8% strongly different pixels, so the check does catch real mistakes.
The converter is free and runs in the browser (disclosure: I built it): SVG to Flutter CustomPainter. It lists anything it could not convert instead of silently dropping it.
Top comments (0)