Flutter — paint dashed line (both horizontal and vertical axis)

Lets create a widget that rendering horizontal or vertical dashed line having these features:

  • Paint style will be customizable. User will be able to define its own paint style.
  • Length of dash and gap will be customizable.
  • And most importantly, the line will be symmetric with respect to the midpoint of line (mathematically formulated) for any size.

If you interested in zigzag line, please read this article Flutter — paint zigzagline (both horizontal and vertical axis)

Let’s start designing an abstract line class, we will extend line pattern classes from this class:

abstract class Line {}

Now, let’s extend two line pattern class: solid and dashed. Our focus will be on dashed line, but in some cases we will use solid line painter which with more performance (always escape from unnecessary calculation).

class SolidLine extends Line {}class DashedLine extends Line {
final double dashSize;
final double gapSize;

DashedLine({this.dashSize, this.gapSize});
}

Let’s design our line painter class — Line painter will take line definition (which is solid or dashed) and the paint style as parameter to produce the requested line.

Let’s design our AdvancedLine widget. Please notice that, our line painter class rendering only horizontal lines, if user requests vertical line, AdvancedLine widget will rotate the rendered horizontal line 90 degrees.

class AdvancedLine extends StatelessWidget {
final Axis direction;
final Line line;
final Paint paintDef;

const AdvancedLine({
Key key,
@required this.direction,
@required this.line,
this.paintDef,
}) : super(key: key);

@override
Widget build(BuildContext context) {
// line painter, always rendering horizontal line
// if requested axis is vertical
// send horizontal line with 90 degree rotated

Paint paint = this.paintDef ?? Paint();

Row lineWrapper = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
height: paint.strokeWidth,
child: CustomPaint(
painter: LinePainter(line: line, paintDef: paint),
),
),
),
],
);

if (direction == Axis.horizontal)
return lineWrapper;
else
return new
RotatedBox(
quarterTurns: 1,
child: lineWrapper,
);
}
}

Let’s build some dashed line outputs:

Column(
children: [
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 240,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(),
),
),
],
),
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(),
paintDef: Paint()..strokeWidth = 2.0,
),
),
),
],
),
SizedBox(height: 25.0),
Row(
children: [
Container(
width: 200,
height: 20.0,
color: Colors.yellow,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 12, gapSize: 6),
paintDef: Paint()..strokeWidth = 5.0,
),
),
],
),
],
),
  • The first line has been defined horizontally (inside 240 width Container) without custom Paint style, with default dash size and gap size. Flutter says for stroke width, ‘Defaults to 0.0, which correspond to a hairline width’.
  • The second line (has wrapped with ‘Expanded widget’, thus the width of the dashed line will be the width of the screen) has been defined with custom Paint stroke width. Please notice that the symmetry of line starting and ending.
  • The last line has been defined (inside 200 width Container) with custom dash size and gap size.

Lest change the Stroke Cap as round or square. ⚡ You can visit flutter documentation page about Stroke Cap types.

Column(
children: [
SizedBox(height: 25.0),
Row(
children: [
Container(
width: 240,
height: 20.0,
color: Colors.yellow,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 8, gapSize: 16),
paintDef: Paint()
..strokeWidth = 3.0
..color = Colors.redAccent,
),
),
],
),
SizedBox(height: 25.0),
Row(
children: [
Container(
width: 240,
height: 20.0,
color: Colors.yellow,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 8, gapSize: 16),
paintDef: Paint()
..strokeWidth = 3.0
..strokeCap = StrokeCap.round,
),
),
],
),
SizedBox(height: 25.0),
Row(
children: [
Expanded(
child: Container(
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 8, gapSize: 16),
paintDef: Paint()
..strokeWidth = 3.0
..strokeCap = StrokeCap.square,
),
),
),
],
),
],
),
  • The first line has been defined with default Stroke Cap which is ‘butt’.
  • The second line is defined by the Stroke Cap ‘round’, the gap and dash sizes are equal to those of the first line. See the effect of changing the stroke cap.
  • The last line has been defined with Stroke Cap ‘square’ and inside Expanded Container.

Let’s build a vertical dashed line:

Column(
children: [
SizedBox(height: 25.0),
Row(
children: [
Expanded(
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 15, gapSize: 5),
paintDef: Paint()..strokeWidth = 3.0,
),
),
],
),
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: Colors.yellow,
height: 120,
width: 12.0,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.vertical,
line: DashedLine(dashSize: 15, gapSize: 10),
paintDef: Paint()
..strokeWidth = 3.0
..strokeCap = StrokeCap.round,
),
)
],
)
],
),

If you wonder that how i calculate symmetry of the dashed line, you can continue to read the article.

Lets see how figure Perfect size and Leap size from given arguments:

Perfect size = (dash size)*c + (gap size)*(c - 1)
c is dash count.
if ( requested size + gap size ) % (dash size + gap size) = 0, so
the size is perfect size for requested dashed line.
Leap size is requested width - perfect sizeif (leapSize != 0) {
if (gapSize > dashSize && gapSize - leapSize >= dashSize)
position = leapSize / 2;
else
firstDashSize = (dashSize - gapSize + leapSize) / 2;
}
Now we decide whether to add dashes or spaces at the beginning and end of the line with leap size.
Suppose that requested line size is 100,
Dash size = 10, Gap size = 5,
(100 + 5) % 15 = 0, so 100 is perfect size.Lets say Leap Size is the result of Size % (dash size + gap size)If requested size is 105, so leap size will be 5.
(105 + gap size) % (dash size + gap size) => 110 % 15 = 5
See image below, the line is not symmetric.
Thus, we can say that if leap size is not zero, the line would not be symmetric. See image below.So how we will solve the symmetry problem?
That is the solution:If gap size less than or equal to dash size, we have to calculate that leap is contain how much gap and how much dash.Split (the sum of the first dash line and dash line inside leap) into two equal dash line, place one of them at the beginning and other at the end.firstDashSize = (dashSize - gapSize + leapSize) / 2;---If gap size bigger than dash size and the difference between gap size and leap size is less than dash size, we have to split the sum of first dash and the last dash into two equal dash line, place one of them at the beginning and other at the end.If requested size is 285, dash size is 10 and gap size is 25, so leap size will be (285 + 25) % 35 = 30, 25 - 30 = -5 which is smaller than dash size.firstDashSize = (dashSize - gapSize + leapSize) / 2 = 7.5---If gap size bigger than dash size and the difference between gap size and leap size is equal or bigger than dash size, we have to start the line with a gap which leap size / 2 length. Thus, we will split the leap size to be both at the beginning and at the end of line as a gap.If requested size is 270, dash size is 10 and gap size is 25, so leap size will be (270 + 25) % 35 = 15, 25 - 15 = 10 which is equal or bigger than dash size.
Thus we have to start the line with leap size / 2 = 7.5 gap
---Column(
children: [
SizedBox(height: 25.0),
Row(
children: [
Container(
width: 290,
// perfect size
height: 25.0,
color: Colors.orangeAccent,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 10, gapSize: 25),
paintDef: Paint()..strokeWidth = 3.0,
),
),
],
),
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 25.0,
width: 285,
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 10, gapSize: 25),
paintDef: Paint()..strokeWidth = 3.0,
),
),
],
),
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 270,
height: 50.0,
color: Colors.redAccent,
alignment: Alignment.center,
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 10, gapSize: 25),
paintDef: Paint()..strokeWidth = 3.0,
),
),
],
),
SizedBox(height: 25.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: AdvancedLine(
direction: Axis.horizontal,
line: DashedLine(dashSize: 12, gapSize: 6),
paintDef: Paint()..strokeWidth = 4.0,
),
),
],
),
],
),
// see the output below

Best regards…

Java, Flutter — Dart Software Developer