@@ -88,8 +88,17 @@ class StrokePainter extends CustomPainter {
8888
8989 // If the stroke is dotted, we take a completely different rendering
9090 // route.
91+ // A pattern that never advances would loop forever, and draws nothing.
92+ if (dashPattern.isNotEmpty &&
93+ dashPattern.fold <double >(0 , (sum, dash) => sum + max (0 , dash)) <= 0 ) {
94+ return ;
95+ }
9196 if (dashPattern.isNotEmpty && strokeSide != StrokeSide .all) {
9297 _paintDashedSide (canvas, size, paint);
98+ } else if (dashPattern.isNotEmpty &&
99+ strokeCap != StrokeCapEnum .none &&
100+ strokeWidth > 0 ) {
101+ _paintCappedDashes (canvas, size, paint, isLine, isHorizLine, isVertLine);
93102 } else if (dashPattern.isNotEmpty) {
94103 final Path path = Path ();
95104
@@ -136,7 +145,9 @@ class StrokePainter extends CustomPainter {
136145 while (totalDistance < metric.length) {
137146 final double dashDistance = dashArray.nextValue ();
138147
139- if (draw) {
148+ // A butt-capped dash of no length covers nothing (a stroke width of
149+ // zero would paint a hairline instead).
150+ if (draw && dashDistance > 0 ) {
140151 // Now that we're about to draw a singular dash, we need to do it
141152 // manually. We do this with tangent calculations. Luckily for us,
142153 // Flutter has a convenient method for that!
@@ -239,6 +250,21 @@ class StrokePainter extends CustomPainter {
239250 };
240251 final double length = (end - start).distance;
241252 if (length <= 0 || dashPattern.every ((dash) => dash <= 0 )) return ;
253+ if (strokeCap != StrokeCapEnum .none && strokeWidth > 0 ) {
254+ // Round and square caps extend every dash by half the stroke width,
255+ // so even a dash of zero length draws a dot.
256+ paint
257+ ..style = PaintingStyle .stroke
258+ ..strokeWidth = strokeWidth;
259+ _paintDashesAlong (
260+ canvas,
261+ Path ()
262+ ..moveTo (start.dx, start.dy)
263+ ..lineTo (end.dx, end.dy),
264+ paint,
265+ );
266+ return ;
267+ }
242268 final LoopingList <double > dashes = LoopingList (dashPattern);
243269 final Offset direction = (end - start) / length;
244270 paint
@@ -261,6 +287,123 @@ class StrokePainter extends CustomPainter {
261287 }
262288 }
263289
290+ /// Paints a dashed stroke on every side whose dashes have round or square
291+ /// caps, as SVG and Figma draw them: each dash runs along the stroke's
292+ /// center line and its caps extend it by half the stroke width at both
293+ /// ends, so a dash of zero length is a dot as wide as the stroke.
294+ ///
295+ /// Dashes without caps are painted as "needles" across the stroke (see
296+ /// [paint] ) instead; caps on those would stick out of the stroke's band.
297+ void _paintCappedDashes (
298+ Canvas canvas,
299+ Size size,
300+ Paint paint,
301+ bool isLine,
302+ bool isHorizLine,
303+ bool isVertLine,
304+ ) {
305+ final Path path = Path ();
306+ if (isLine) {
307+ path
308+ ..moveTo (
309+ isHorizLine ? strokeWidth / 2 : 0 ,
310+ isVertLine ? strokeWidth / 2 : 0 ,
311+ )
312+ ..lineTo (
313+ isHorizLine ? strokeWidth / 2 : size.width,
314+ isVertLine ? strokeWidth / 2 : size.height,
315+ );
316+ } else {
317+ // The center line of the stroke: half its width inside the box for an
318+ // inside stroke, on the edge for a centered one, and half its width
319+ // outside for an outside one.
320+ final double grow = switch (strokeAlign) {
321+ StrokeAlignC .inside => - strokeWidth / 2 ,
322+ StrokeAlignC .center => 0 ,
323+ StrokeAlignC .outside => strokeWidth / 2 ,
324+ };
325+ if (boxShape == BoxShape .circle) {
326+ final double diameter = min (size.width, size.height) + grow * 2 ;
327+ if (diameter <= 0 ) return ;
328+ path.addOval (
329+ Rect .fromCenter (
330+ center: Offset (size.width / 2 , size.height / 2 ),
331+ width: diameter,
332+ height: diameter,
333+ ),
334+ );
335+ } else {
336+ final RRect box = borderRadius
337+ .toRRect (Rect .fromLTWH (0 , 0 , size.width, size.height))
338+ .inflate (grow);
339+ if (box.width <= 0 || box.height <= 0 ) return ;
340+ path.addRRect (box);
341+ }
342+ }
343+ paint
344+ ..style = PaintingStyle .stroke
345+ ..strokeWidth = strokeWidth;
346+ _paintDashesAlong (canvas, path, paint);
347+ }
348+
349+ /// Paints [dashPattern] along [path] with [paint] , whose cap is round or
350+ /// square. Dashes shorter than [_dotLength] are painted as dots: the cap
351+ /// alone, which a stroke of (almost) no length may not draw.
352+ void _paintDashesAlong (Canvas canvas, Path path, Paint paint) {
353+ final LoopingList <double > dashes = LoopingList (dashPattern);
354+ final Paint dotPaint = Paint ()
355+ ..color = paint.color
356+ ..style = PaintingStyle .fill
357+ ..isAntiAlias = paint.isAntiAlias;
358+ for (final PathMetric metric in path.computeMetrics ()) {
359+ double distance = 0 ;
360+ bool draw = true ;
361+ while (distance < metric.length) {
362+ final double dash = max (0.0 , dashes.nextValue ());
363+ if (draw) {
364+ final double end = min (distance + dash, metric.length);
365+ if (end - distance < _dotLength) {
366+ final Tangent ? tangent = metric.getTangentForOffset (distance);
367+ if (tangent != null ) _paintDot (canvas, tangent, dotPaint);
368+ } else {
369+ canvas.drawPath (metric.extractPath (distance, end), paint);
370+ }
371+ }
372+ distance += dash;
373+ draw = ! draw;
374+ }
375+ }
376+ }
377+
378+ /// Dashes shorter than this are painted as a dot of the cap's shape.
379+ static const double _dotLength = 0.01 ;
380+
381+ /// Paints a dash of no length at [tangent] : a circle as wide as the
382+ /// stroke for a round cap, a square turned along the path for a square
383+ /// one.
384+ void _paintDot (Canvas canvas, Tangent tangent, Paint paint) {
385+ final double radius = strokeWidth / 2 ;
386+ if (strokeCap == StrokeCapEnum .round) {
387+ canvas.drawCircle (tangent.position, radius, paint);
388+ return ;
389+ }
390+ final double length = tangent.vector.distance;
391+ final Offset along = length == 0
392+ ? Offset (radius, 0 )
393+ : tangent.vector / length * radius;
394+ final Offset across = Offset (- along.dy, along.dx);
395+ final Offset center = tangent.position;
396+ canvas.drawPath (
397+ Path ()
398+ ..moveTo ((center - along - across).dx, (center - along - across).dy)
399+ ..lineTo ((center + along - across).dx, (center + along - across).dy)
400+ ..lineTo ((center + along + across).dx, (center + along + across).dy)
401+ ..lineTo ((center - along + across).dx, (center - along + across).dy)
402+ ..close (),
403+ paint,
404+ );
405+ }
406+
264407 @override
265408 bool shouldRepaint (covariant StrokePainter oldDelegate) =>
266409 oldDelegate.borderRadius != borderRadius ||
0 commit comments