文章目录
简介
Flutter中如何使用虚线
使用的是第三方库
Flutter-Dotted-Border其中的封装了基本的图形样式,矩形、圆角矩形、圆形、椭圆形,以及自定义虚线框。内部其实是使用了
path_drawing这个库作为绘制虚线。
Flutter-Dotted-Border
其中就只有两个文件dotted_border.dart和dash_printer.dart
基本使用
Widget get rectBorderWidget {
return DottedBorder(
dashPattern: [8, 4],
strokeWidth: 2,
padding: EdgeInsets.all(0),
child: Container(
height: 200,
width: 120,
color: Colors.red,
// color: Colors.black26,
),
);
}
dashPattern: [8, 4],第一个参数表示虚线长度,第二参数表示空格长度。
DottedBorder虚线边框封装核心代码分析
class DottedBorder extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
final double strokeWidth;
final Color color;
final List<double> dashPattern;
final BorderType borderType;
final Radius radius;
final PathBuilder customPath;
final StrokeCap strokeCap;
DottedBorder({
@required this.child,
this.color = Colors.black,
this.strokeWidth = 1,
this.borderType = BorderType.Rect,
this.dashPattern = const <double>[3, 1],
this.padding = const EdgeInsets.all(2),
this.radius = const Radius.circular(0),
this.customPath,
this.strokeCap = StrokeCap.butt,
}) {
assert(child != null);
assert(_isValidDashPattern(dashPattern), 'Invalid dash pattern');
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(
child: CustomPaint(
painter: _DashPainter(
strokeWidth: strokeWidth,
radius: radius,
color: color,
borderType: borderType,
dashPattern: dashPattern,
customPath: customPath,
strokeCap: strokeCap,
),
),
),
Padding(
padding: padding,
child: child,
),
],
);
}
}
最外层使用了Stack,将两个组件组合在一起,Positioned.fill()用来控制CustomPaint中的宽高。这样做的好处,可以让child的视图显示在CustomPaint绘制的上层。
思考
如果不使用Stack,而是直接使用CustomPaint中的child参数传递子组件,会是什么效果?
CustomPaint(
painter: _DashPainter(
strokeWidth: strokeWidth,
radius: radius,
color: color,
borderType: borderType,
dashPattern: dashPattern,
customPath: customPath,
strokeCap: strokeCap,
),
child: child,
),
),
答:这样做会导致,CustomPaint绘制在外层,遮挡住child的内容。
_DashPainter封装边框的类
class _DashPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..strokeWidth = strokeWidth
..color = color
..strokeCap = strokeCap
..style = PaintingStyle.stroke;
Path _path;
// 关键在下面,获取虚线的Path
if (customPath != null) {
_path = dashPath(
customPath(size),
dashArray: CircularIntervalList(dashPattern),
);
} else {
_path = _getPath(size);
}
// 将结果绘制出来
canvas.drawPath(_path, paint);
}
}
获取虚线path的方法
Path _getPath(Size size) {
Path path;
switch (borderType) {
...
case BorderType.Rect:
path = _getRectPath(size);
break;
...
}
return dashPath(path, dashArray: CircularIntervalList(dashPattern));
}
Path _getRectPath(Size size) {
return Path()
..addRect(
Rect.fromLTWH(
0,
0,
size.width,
size.height,
),
);
}
其实就是根据边框类型,先绘制了一个实线的边框,关键是调用dashPath(path, dashArray: CircularIntervalList(dashPattern)); 得到虚线边框。
绘制虚线的核心库path_drawing
path_drawing.dart主入口文件
export 'package:path_drawing/src/parse_path.dart';
export 'package:path_drawing/src/dash_path.dart';
export 'package:path_drawing/src/trim_path.dart';
其实就只有3个有用的文件
- dash_path绘制虚线
- parse_path解析svg成path
- trim_path裁剪path
这里只分析dash_path.dart,绘制虚线的类。
dash_path.dart绘制虚线核心类
Path dashPath(
Path source, {
@required CircularIntervalList<double> dashArray,
DashOffset dashOffset,
}) {
assert(dashArray != null);
if (source == null) {
return null;
}
// 默认从path的起点开始绘制
dashOffset = dashOffset ?? const DashOffset.absolute(0.0);
// TODO: Is there some way to determine how much of a path would be visible today?
final Path dest = Path();
// 计算完整的线段的长度
for (final PathMetric metric in source.computeMetrics()) {
double distance = dashOffset._calculate(metric.length);
// draw用来取反,为true表示绘制该线段,false表示不绘制
bool draw = true;
while (distance < metric.length) {
// dashArray其实就是dashPattern: [8, 4]的值
final double len = dashArray.next;
if (draw) {
// 不断的调整偏移量,绘制线段
dest.addPath(metric.extractPath(distance, distance + len), Offset.zero);
}
distance += len;
draw = !draw;
}
}
return dest;
}
- dashOffset获取开始绘制虚线的偏移量,默认0,也就是从头开始绘制。
- 先计算边框的path总长度。
- 根据dashArray来获取虚线、空格的数据。
- 获取总path的偏移量,也就是小线段(虚线)。
- 通过draw控制绘制间隔。
- 返回最终的dest,就是通过偏移、绘制小线段形成的虚线。
CustomPaint的size属性研究
有size或child
CustomPaint(
// size: Size(200.0, 200.0),
painter: TestPainter(),
child: Container(
padding: EdgeInsets.all(0),
height: 200,
width: 120,
color: Colors.black26,
child: Text("内部", style: TextStyle(color: Colors.redAccent),),
),
);
/// The size that this [CustomPaint] should aim for, given the layout
/// constraints, if there is no child.
///
/// Defaults to [Size.zero].
///
/// If there's a child, this is ignored, and the size of the child is used
/// instead.
final Size size;
size属性,文档已经说明了。默认是0,如果设置了child,则size属性无效,以child宽高为准。
没有size和child
如果是没有给定size,也没有给child。则依然可以根据TestPainter绘制出图形,但这个图形没事实际的宽高,也就是说在布局的时候无法提供一个宽高值给其他组件参考,
class PaintView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: TestPainter(),
);
}
}
class TestPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print("打印size.width = ${size.width} size.height = ${size.height}");
Paint paint = Paint()
..strokeWidth = 2
..color = Colors.black
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
Path path = Path()
..addRect(
Rect.fromLTWH(
0,
0,
40,
200,
),
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
// padding: const EdgeInsets.all(0.0),
child: Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
// spacing: 0,
children: <Widget>[
PaintView(),
Text("测试布局位置"),
],
),
),
),
结果可以看到,没有给size也没有给child,则实际PaintView是没有宽高的,但依然能绘制出图形,这会造成Text(“测试布局位置”)的布局无法定位在矩形的右侧。在使用的过程一定要注意!
总结
Flutter-Dotted-Border 封装了基本的虚线边框。
path_drawing 实际绘制虚线的核心库,本质是利用了path的长度,计算每一个小线段的偏移量,将多个小线段合并成一个完整的虚线边框。