CustomPaint 是一个 Widget 类,提供了绘制时所需的画布。
const CustomPaint({
Key? key,
// CustomPainter 绘制背景
this.painter,
// CustomPainter 绘制前景
this.foregroundPainter,
// 尺寸,默认为0,如果有子widget则为子widget的大小
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
})
CustomPainter 一个用于实现CustomPaint绘制的接口,需要实现此接口来进行自定义绘制。
class _ScanFramePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 绘图的主要实现
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// 返回true 则会重新绘制,执行 paint函数,返回false 则不会重新绘制
return true;
}
}
Paint 画笔
Canvas 画布
两者组合可绘制各种图形。
1. 自定义 CustomPainter
class TextPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = Colors.blue;
// size是widget的尺寸,即CustomPaint的尺寸
canvas.drawRect(Rect.fromLTRB(0, 0, size.width, size.height), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
2. 显示CustomPaint以及Text
class CustomTextWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomPaint(
painter: TextPainter(),
child: Text('Flutter 画笔绘制二维码扫描框'),
),
),
);
}
}
先看效果图,然后根据 xy轴以及扫描框的大小计算坐标。
class _ScanFramePainter extends CustomPainter {
//默认定义扫描框为 260边长的正方形
final Size frameSize = Size.square(260.0);
@override
void paint(Canvas canvas, Size size) {
// 按扫描框居中来计算,全屏尺寸与扫描框尺寸的差集 除以 2 就是扫描框的位置
Offset diff = size - frameSize;
double leftTopX = diff.dx / 2;
double leftTopY = diff.dy / 2;
//根据左上角的坐标和扫描框的大小可得知扫描框矩形
var rect =
Rect.fromLTWH(leftTopX, leftTopY, frameSize.width, frameSize.height);
// 4个点的坐标
Offset leftTop = rect.topLeft;
Offset leftBottom = rect.bottomLeft;
Offset rightTop = rect.topRight;
Offset rightBottom = rect.bottomRight;
//定义画笔
Paint paint = Paint()
..color = Colors.blue //颜色
..strokeWidth = 4.0 //画笔线条宽度
..style = PaintingStyle.stroke; // 画笔的模式,填充还是只绘制边框
//绘制正方形
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
调整 CustomPaint 的尺寸后查看效果
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: _ScanFramePainter(),
child: Container(),//全屏
),
);
}
绘制方框只是为了查看扫描框位置是否正确
除扫描框以外,分成四个矩形组成在一起就是扫描框背景,在paint函数中绘制扫描背景
//绘制罩层
//画笔
Paint paint = Paint()
..color = Color(0x40cccccc) //透明灰
..style = PaintingStyle.fill; // 画笔的模式,填充
//左侧矩形
canvas.drawRect(Rect.fromLTRB(0, 0, leftTopX, size.height), paint);
//右侧矩形
canvas.drawRect(
Rect.fromLTRB(rightTop.dx, 0, size.width, size.height),
paint,
);
//中上矩形
canvas.drawRect(Rect.fromLTRB(leftTopX, 0, rightTop.dx, leftTopY), paint);
//中下矩形
canvas.drawRect(
Rect.fromLTRB(leftBottom.dx, leftBottom.dy, rightBottom.dx, size.height),
paint,
);
定义边角的长度,和扫描线的移动高度
final double cornerLength = 20.0;
在paint函数中继续绘制扫描框
// 重新设置画笔
paint
..color = Colors.blue
..strokeWidth = 4.0
..strokeCap = StrokeCap.square // 解决因为线宽导致交界处不是直角的问题
..style = PaintingStyle.stroke;
// 横向线条的坐标偏移
Offset horizontalOffset = Offset(cornerLength, 0);
// 纵向线条的坐标偏移
Offset verticalOffset = Offset(0, cornerLength);
// 左上角
canvas.drawLine(leftTop, leftTop + horizontalOffset, paint);
canvas.drawLine(leftTop, leftTop + verticalOffset, paint);
// 左下角
canvas.drawLine(leftBottom, leftBottom + horizontalOffset, paint);
canvas.drawLine(leftBottom, leftBottom - verticalOffset, paint);
// 右上角
canvas.drawLine(rightTop, rightTop - horizontalOffset, paint);
canvas.drawLine(rightTop, rightTop + verticalOffset, paint);
// 右下角
canvas.drawLine(rightBottom, rightBottom - horizontalOffset, paint);
canvas.drawLine(rightBottom, rightBottom - verticalOffset, paint);
首先要确定扫描线的Y坐标,由外部传值
_ScanFramePainter({
this.lineMoveValue}) : assert(lineMoveValue != null);
// 百分比值,0 ~ 1,然后计算Y坐标
final double lineMoveValue;
在paint函数中继续绘制扫描线
//修改画笔线条宽度
paint.strokeWidth = 2;
// 扫描线的移动值
var lineY = leftTopY + frameSize.height * lineMoveValue;
// 10 为线条与方框之间的间距,绘制扫描线
canvas.drawLine(
Offset(leftTopX + 10.0, lineY),
Offset(rightTop.dx - 10.0, lineY),
paint,
);
使用 Animation 实现线条移动
class _ScanFrameState extends State<ScanFrame> with TickerProviderStateMixin {
Animation<double> _animation;
AnimationController _controller;
//起始之间的线性插值器 从 0.05 到 0.95 百分比。
final Tween<double> _rotationTween = Tween(begin: 0.05, end: 0.95);
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, //实现 TickerProviderStateMixin
duration: Duration(seconds: 3), //动画时间 3s
);
_animation = _rotationTween.animate(_controller)
..addListener(() => setState(() {
}))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.repeat();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.repeat();
}
@override
void dispose() {
// 释放动画资源
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _ScanFramePainter(lineMoveValue: _animation.value),
child: Container(),
);
}
}
效果是实现了,不过扫描线看起来不够流畅,后续在做修改。