flutter 中的自定义 Widget 算做是 flutter 体系中比较高阶的知识点之一了,至关于原生开发中的自定义 View,以我我的的感觉来讲,自定义 widget 的难度要低于自定义 View,不过因为当前 flutter 的开源库还不算多丰富,因此有些效果仍是须要开发者本身动手来实现,而本篇文章就来介绍如何用 flutter 来实现一个带文本的波浪球 Widget,实现的的效果以下所示:git
源代码点击这里下载:github.com/leavesC/flu…github
先来总结下该 WaveLoadingWidget 的特色,这样才能概括出实现该效果所需的步骤canvas
虽然波浪是不断运动的,但只要可以绘制出其中一帧的图形,其动态效果就能经过不断改变波浪的位置参数来完成,因此这里先把该 widget 当成静态的,先实现其静态效果便可ide
将绘制步骤拆解为如下几步:布局
canvas.clipPath(targetPath)
方法裁切画布,再绘制颜色为 foregroundColor 的文本,此时绘制的 foregroundColor 文本只会显示 targetPath 范围内的部分,从而使两次不一样时间绘制的文本重叠在了一块儿,获得了有不一样颜色范围的文本如今就来一步步实现以上的绘制步骤吧字体
flutter 经过抽象类 CustomPainter
为开发者提供了自绘 UI 的入口,其内部的抽象方法 void paint(Canvas canvas, Size size)
提供了画布对象 canvas 以及包含 widget 宽高信息的 size 对象动画
此处就来继承 CustomPainter 类,初始化画笔对象以及各个配置参数(要绘制的文本,颜色值等)ui
class WaveLoadingPainter extends CustomPainter {
//若是外部没有指定颜色值,则使用此默认颜色值
static final Color defaultColor = Colors.lightBlue;
//画笔对象
var _paint = Paint();
//圆形路径
Path _circlePath = Path();
//波浪路径
Path _wavePath = Path();
//要显示的文本
final String text;
//字体大小
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
WaveLoadingPainter(
{this.text,
this.fontSize,
this.backgroundColor,
this.foregroundColor,
this.waveColor}) {
_paint
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 3
..color = waveColor ?? defaultColor;
}
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
复制代码
flutter 的 canvas 对象没有提供直接 drawText
的 API,其绘制文本的步骤相对原生的自定义 View 要比较麻烦this
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
···
}
void _drawText({Canvas canvas, double side, Color colors}) {
ParagraphBuilder pb = ParagraphBuilder(ParagraphStyle(
textAlign: TextAlign.center,
fontStyle: FontStyle.normal,
fontSize: fontSize ?? 0,
));
pb.pushStyle(ui.TextStyle(color: colors ?? defaultColor));
pb.addText(text);
ParagraphConstraints pc = ParagraphConstraints(width: fontSize ?? 0);
Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(
paragraph,
Offset(
(side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0));
}
复制代码
取 widget 的宽和高的最小值做为圆的直径大小,以此构建出一个不超出 widget 范围的最大圆形路径spa
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
···
}
复制代码
此处波浪的宽度和高度就根据一个固定的比例值来求值,以 _circlePath 的中间分隔线做为水平线,在水平线上下根据贝塞尔曲线绘制出连续的波浪线
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
double waveWidth = side * 0.8;
double waveHeight = side / 6;
_wavePath.reset();
_wavePath.moveTo(-waveWidth, radius);
for (double i = -waveWidth; i < side; i += waveWidth) {
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, -waveHeight, waveWidth / 2, 0);
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
//为了方便读者理解,这里把路径绘制出来,实际上不须要
canvas.drawPath(_wavePath, _paint);
}
复制代码
此时绘制的曲线还处于非闭合状态,须要将 _wavePath 的首尾两端链接起来,这样才能够和 _circlePath 作交集
_wavePath.relativeLineTo(0, radius);
_wavePath.lineTo(-waveWidth, side);
_wavePath.close();
复制代码
_wavePath 闭合后,此时绘制出来的图形就以下所示
_circlePath 和 _wavePath 的交集就是一个半圆形波浪了
var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath);
canvas.drawPath(combine, _paint);
//为了方便读者理解,这里把路径绘制出来,实际上不须要
canvas.drawPath(combine, _paint);
复制代码
文本的颜色是分为上下两部分的,foregroundColor 颜色的文本不须要显示上半部分,因此在绘制 foregroundColor 文本的时候须要把上半部分文本给裁切掉,使两次不一样时间绘制的文本重叠在了一块儿,获得了有不一样颜色范围的文本
canvas.clipPath(combine);
_drawText(canvas: canvas, side: side, colors: foregroundColor);
复制代码
如今已经绘制好了单独一帧时的效果图了,能够考虑使 widget 动起来了
只要不断改变贝塞尔曲线的起始点坐标,使之不断从左往右移动,就能够营造出波浪从左往右前进的效果了。WaveLoadingPainter 只负责根据外部传入的动画值 animatedValue 来绘制 UI,构造 animatedValue 的逻辑则由外部的 _WaveLoadingWidgetState 进行处理,这里规定 animatedValue 的值是从 0 递增到 1,在开始构建 _wavePath 前只须要移动其起始坐标点便可
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
double waveWidth = side * 0.8;
double waveHeight = side / 6;
_wavePath.reset();
_wavePath.moveTo((animatedValue - 1) * waveWidth, radius);
for (double i = -waveWidth; i < side; i += waveWidth) {
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, -waveHeight, waveWidth / 2, 0);
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
_wavePath.relativeLineTo(0, radius);
_wavePath.lineTo(-waveWidth, side);
_wavePath.close();
var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath);
canvas.drawPath(combine, _paint);
canvas.clipPath(combine);
_drawText(canvas: canvas, side: side, colors: foregroundColor);
}
复制代码
class _WaveLoadingWidgetState extends State<WaveLoadingWidget> with SingleTickerProviderStateMixin {
final String text;
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
AnimationController controller;
Animation<double> animation;
_WaveLoadingWidgetState(
{@required this.text,
@required this.fontSize,
@required this.backgroundColor,
@required this.foregroundColor,
@required this.waveColor});
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
controller.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
print("dismissed");
break;
case AnimationStatus.forward:
print("forward");
break;
case AnimationStatus.reverse:
print("reverse");
break;
case AnimationStatus.completed:
print("completed");
break;
}
});
animation = Tween(
begin: 0.0,
end: 1.0,
).animate(controller)
..addListener(() {
setState(() => {});
});
controller.repeat();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WaveLoadingPainter(
text: text,
fontSize: fontSize,
animatedValue: animation.value,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
waveColor: waveColor,
),
);
}
}
复制代码
以后只要将 WaveLoadingPainter 包裹到 StatefulWidget 中便可,在 StatefulWidget 中开放能够自定义配置的参数就能够了
class WaveLoadingWidget extends StatefulWidget {
final String text;
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
WaveLoadingWidget(
{@required this.text,
@required this.fontSize,
@required this.backgroundColor,
@required this.foregroundColor,
@required this.waveColor}) {
assert(text != null && text.length == 1);
assert(fontSize != null && fontSize > 0);
}
@override
_WaveLoadingWidgetState createState() => _WaveLoadingWidgetState(
text: text,
fontSize: fontSize,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
waveColor: waveColor,
);
}
复制代码
使用方式就相似于通常的系统 widget
Container(
width: 300,
height: 300,
child: WaveLoadingWidget(
text: "锲",
fontSize: 215,
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
waveColor: Colors.lightBlue,
),
),
Container(
width: 250,
height: 250,
child: WaveLoadingWidget(
text: "而",
fontSize: 175,
backgroundColor: Colors.indigoAccent,
foregroundColor: Colors.white,
waveColor: Colors.indigoAccent,
),
),
复制代码
此外该项目也提供了 N 多个经常使用 Widget 和自定义 Widget 的使用及实现方法,涵盖了系统 Widget 、布局容器、动画、高阶功能、自定义 Widget 等内容,欢迎 star