创建canvas画板:
return CustomPaint(
size: Size(100, 100),
painter: WavePainter(
progress: 0.5,
waveColor: Colors.blue,
),
)
创建一个WavePainter继承CustomPainter:
class WavePainter extends CustomPainter {
final double progress;
final Color waveColor;
WavePainter({
required this.progress,
required this.waveColor,
});
final Paint wavePaint = Paint();
double painterHeight = 0; // 波浪总体高度
double waveWidth = 0; // 波浪宽度
double waveHeight = 0; // 波浪浪尖高度
@override
void paint(Canvas canvas, Size size) {
painterHeight = size.height;
waveWidth = size.width / 2;
waveHeight = size.height * 0.06;
// 绘制波浪
drawWave(
canvas,
Offset(-4 * waveWidth,
painterHeight + waveHeight),
waveColor,
);
}
Path drawWave(Canvas canvas, Offset startPoint, Color color) {
Path wavePath = Path();
wavePath.moveTo(startPoint.dx, startPoint.dy);
wavePath.relativeLineTo(0, -painterHeight * progress);
int waveCount = 3;
for (int i = 0; i < waveCount; i++) {
wavePath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
wavePath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
}
wavePath.relativeLineTo(0, painterHeight);
wavePath.relativeLineTo(-waveWidth * waveCount * 2.0, 0);
canvas.drawPath(wavePath, wavePaint..color = color);
return wavePath;
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return false;
}
}
绘制波浪的方法写在drawWave中,在CustomPaint外面套一个Container看下效果先:
Container的clipBehavior属性去掉可以砍出波浪的具体位置,现在波浪是静止的
下面波浪动起来,给WavePainter传入一个Animation<double>动画:
AnimationController _waveCtrl;
_waveCtrl = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
)..repeat();
WavePainter(
waveColor: widget.waveColor,
progress: progress,
flow: _waveCtrl,
)
class WavePainter extends CustomPainter {
final double progress;
final Color waveColor;
final Animation<double> flow;
WavePainter({
required this.progress,
required this.waveColor,
required this.flow,
}) : super(repaint: flow);
// 省略重复代码...
@override
void paint(Canvas canvas, Size size) {
// 省略重复代码...
// 绘制波浪
drawWave(
canvas,
Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor,
);
}
Path drawWave(Canvas canvas, Offset startPoint, Color color) {
// 省略重复代码...
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate.flow != flow;
}
}
再来一道底波:
@override
void paint(Canvas canvas, Size size) {
// 省略重复代码...
// 绘制波浪
drawWave(
canvas,
Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor,
);
// 绘制底波
drawWave(
canvas,
Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor.withAlpha(80),
);
}
底波横向移动速度翻倍4 * waveWidth * flow.value,透明度80%
外层Container的clipBehavior属性改为Clip.antiAlias,再动态更新progress的值:
接下来加上文字:
@override
void paint(Canvas canvas, Size size) {
// 省略重复代码...
// 绘制文字
drawText(canvas, size, textColor);
// 绘制波浪
drawWave(
canvas,
Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor,
);
// 绘制底波
drawWave(
canvas,
Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor.withAlpha(80),
);
}
void drawText(Canvas canvas, Size size, Color color) {
// 文字内容
String text = '加载中...';
// 文字样式
TextStyle textStyle = TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: color,
);
// 最大行数
int maxLines = 2;
// 文字画笔
_textPainter
..text = TextSpan(
text: text,
style: textStyle,
)
..maxLines = maxLines
..textDirection = TextDirection.ltr;
// 绘制文字
_textPainter.layout(maxWidth: size.width);
// 文字Size
Size textSize = _textPainter.size;
_textPainter.paint(
canvas,
Offset(
(size.width - textSize.width) / 2,
size.height / 2 + (size.height / 2 - textSize.height) / 2,
),
);
}
文字绘制完发现和波浪混一起就看不到了
中间尝试过用blendMode和colorFilter让文字和波浪重叠的部分混色,效果不太理想
采取另一种办法,绘制两遍文字,波浪上方绘制一遍,波浪下方绘制一遍:
@override
void paint(Canvas canvas, Size size) {
// 省略重复代码...
// 绘制波浪上方文字
drawText(canvas, size, textColor);
// 绘制波浪
Path wavePath = drawWave(
canvas,
Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor,
);
// 绘制底波
drawWave(
canvas,
Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor.withAlpha(80),
);
// 绘制波浪下方文字
canvas.clipPath(wavePath);
drawText(canvas, size, Colors.white);
}
绘制波浪下方文字时用clipPath沿着波浪wavePath裁剪了一下,这样文字就只在波浪上显示了,不会超出波浪范围:
完整代码,对波浪组件进行了封装:
import 'dart:async';
import 'package:flutter/material.dart';
class WaveLoading extends StatefulWidget {
// 进度
final double progress;
// 波浪颜色
final Color waveColor;
// 尺寸
final Size size;
// 圆角半径
final double borderRadius;
// 动画时长
final Duration duration;
// 文字
final String text;
// 字号
final double fontSize;
// 文字颜色
final Color? textColor;
// 是否需要省略号
final bool needEllipsis;
const WaveLoading({
Key? key,
this.progress = 0.6,
this.waveColor = Colors.blue,
this.size = const Size(100, 100),
this.borderRadius = 0,
this.duration = const Duration(seconds: 1),
this.text = '加载中',
this.fontSize = 15,
this.textColor,
this.needEllipsis = true,
}) : super(key: key);
@override
State<WaveLoading> createState() => _WaveLoadingState();
}
class _WaveLoadingState extends State<WaveLoading>
with TickerProviderStateMixin {
late AnimationController _waveCtrl;
Timer? _timer;
ValueNotifier<int> _ellipsisCount = ValueNotifier(1); // 文字省略号点的个数
@override
void initState() {
super.initState();
// 初始化动画控制器
_initAnimationCtrl();
}
@override
void dispose() {
_waveCtrl.dispose();
_timer?.cancel();
super.dispose();
}
// 初始化动画控制器
void _initAnimationCtrl() {
_waveCtrl = AnimationController(
duration: widget.duration,
vsync: this,
)..repeat();
if (widget.needEllipsis) {
// 有省略号才初始化计时器
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_ellipsisCount.value < 3) {
_ellipsisCount.value++;
} else {
_ellipsisCount.value = 1;
}
});
}
}
@override
Widget build(BuildContext context) {
double progress = widget.progress > 1 ? 1 : widget.progress;
return RepaintBoundary(
child: CustomPaint(
size: widget.size,
painter: WavePainter(
waveColor: widget.waveColor,
borderRadius: widget.borderRadius,
progress: progress,
repaint: Listenable.merge([_waveCtrl, _ellipsisCount]),
flow: _waveCtrl,
ellipsisCount: _ellipsisCount,
text: widget.text,
textColor: widget.textColor ?? widget.waveColor,
fontSize: widget.fontSize,
needEllipsis: widget.needEllipsis,
),
),
);
}
}
class WavePainter extends CustomPainter {
final Listenable repaint;
final Animation<double> flow;
final ValueNotifier<int> ellipsisCount;
final double progress;
final Color waveColor;
final double borderRadius;
final String text;
final double fontSize;
final Color textColor;
final bool needEllipsis;
WavePainter({
required this.repaint,
required this.flow,
required this.ellipsisCount,
required this.progress,
required this.waveColor,
required this.borderRadius,
required this.text,
required this.fontSize,
required this.textColor,
required this.needEllipsis,
}) : super(repaint: repaint);
final Paint wavePaint = Paint();
final Paint borderPaint = Paint();
final TextPainter _textPainter = TextPainter();
double painterHeight = 0;
double waveWidth = 0;
double waveHeight = 0;
@override
void paint(Canvas canvas, Size size) {
painterHeight = size.height;
waveWidth = size.width / 2;
waveHeight = size.height * 0.06;
borderPaint
..style = PaintingStyle.fill
..color = waveColor.withAlpha(15);
// 绘制背景
Path borderPath = Path();
borderPath.addRRect(
RRect.fromRectXY(Offset.zero & size, borderRadius, borderRadius));
canvas.clipPath(borderPath);
canvas.drawPath(borderPath, borderPaint);
// 绘制波浪上方文字
drawText(canvas, size, textColor);
// 绘制波浪
Path wavePath = drawWave(
canvas,
Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor,
);
// 绘制底波
drawWave(
canvas,
Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
painterHeight + waveHeight),
waveColor.withAlpha(80),
);
// 绘制波浪下方文字
canvas.clipPath(wavePath);
drawText(canvas, size, Colors.white);
}
Path drawWave(Canvas canvas, Offset startPoint, Color color) {
Path wavePath = Path();
wavePath.moveTo(startPoint.dx, startPoint.dy);
wavePath.relativeLineTo(0, -painterHeight * progress);
int waveCount = 3;
for (int i = 0; i < waveCount; i++) {
wavePath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
wavePath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
}
wavePath.relativeLineTo(0, painterHeight);
wavePath.relativeLineTo(-waveWidth * waveCount * 2.0, 0);
canvas.drawPath(wavePath, wavePaint..color = color);
return wavePath;
}
void drawText(Canvas canvas, Size size, Color color) {
// 文字内容
String content = text;
if (needEllipsis) {
String ellipsis = '.' * ellipsisCount.value;
content += ellipsis.toString();
}
// 文字样式
TextStyle textStyle = TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: color,
);
// 最大行数
int maxLines = 2;
// 文字画笔
_textPainter
..text = TextSpan(
text: content,
style: textStyle,
)
..maxLines = maxLines
..textDirection = TextDirection.ltr;
// 文字Size,如果repaint不为空,说明需要省略号,计算时拼接上三个点,得出最大宽度
Size textSize = sizeWithLabel(
needEllipsis ? text + '...' : text,
textStyle,
maxLines,
);
// 绘制文字
_textPainter.layout(maxWidth: size.width);
_textPainter.paint(
canvas,
Offset(
(size.width - textSize.width) / 2,
size.height / 2 + (size.height / 2 - textSize.height) / 2,
),
);
}
// 计算文字Size
Size sizeWithLabel(String text, TextStyle textStyle, int maxLines) {
TextSpan textSpan = TextSpan(text: text, style: textStyle);
TextPainter textPainter = TextPainter(
text: textSpan, maxLines: maxLines, textDirection: TextDirection.ltr);
textPainter.layout();
return textPainter.size;
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate.repaint != repaint ||
oldDelegate.flow != flow ||
oldDelegate.progress != progress ||
oldDelegate.waveColor != waveColor ||
oldDelegate.borderRadius != borderRadius ||
oldDelegate.text != text ||
oldDelegate.textColor != textColor ||
oldDelegate.fontSize != fontSize ||
oldDelegate.needEllipsis != needEllipsis ||
oldDelegate.ellipsisCount != ellipsisCount;
}
}
使用:
TextButton(
child: Text(
'show',
style: TextStyle(
fontSize: 30,
),
),
onPressed: () {
showDialog(
context: context,
barrierDismissible: true,
barrierColor: Colors.transparent,
builder: (context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
// 阴影
boxShadow: const [
BoxShadow(
color: Colors.grey,
offset: Offset(0, 1), // 阴影xy轴偏移量
blurRadius: 0.1, // 阴影模糊程度
spreadRadius: 0.1, // 阴影扩散程度
),
],
),
clipBehavior: Clip.antiAlias,
child: Obx(() {
// 这里为了实时刷新,使用了Getx状态管理框架,换成其他方式亦可
return WaveLoading(
size: Size(200.w, 200.w),
progress: progress.value / 100,
text: '${progress.value}%',
fontSize: 36.sp,
needEllipsis: false,
);
}),
),
);
},
).then((value) {
progress.value = 0;
timer?.cancel();
});
// 模拟progress更新
timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
if (progress.value < 100) {
progress.value++;
}
// debugPrint(progress.value.toString());
});
},
)