时间:2022-08-11 11:28:10 | 栏目:Android代码 | 点击:次
当 flutter 的现有组件无法满足产品要求的 UI 效果时,我们就需要通过自绘组件的方式来进行实现了。本篇文章就来介绍如何用 flutter 自定义实现一个带文本的波浪球,效果如下所示:
先来总结下 WaveLoadingWidget 的特点,这样才能归纳出实现该效果所需要的步骤:
虽然文本的整体颜色是在不断变化的,但只要能够绘制出其中一帧的图形,其动态效果就能通过不断改变波浪曲线的位置参数来实现,所以这里先把该 widget 当成静态的,先实现其静态效果即可
将绘制步骤拆解为以下几步:
canvas.clipPath(combinePath)
方法裁切画布,再绘制颜色为 foregroundColor 的文本,此时绘制的 foregroundColor 文本只会显示 combinePath 范围内的部分,也即只会显示下半部分,使得两次不同时间绘制的文本重叠在了一起,从而得到了有不同颜色范围的文本现在就来一步步实现以上的绘制步骤吧
flutter 通过 CustomPainter 为开发者提供了自绘 UI 的入口,其内部的 void paint(Canvas canvas, Size size)
方法提供了画布 canvas 对象以及包含 widget 宽高信息的 size 对象
这里就来继承 CustomPainter 类,在 paint
方法中先来绘制颜色为 backgroundColor 的文本。flutter 的 canvas 对象没有提供直接 drawText
的 API,所以其绘制文本的步骤相对原生的自定义 View 要稍微麻烦一点
class _WaveLoadingPainter extends CustomPainter { final String text; final double fontSize; final double animatedValue; final Color backgroundColor; final Color foregroundColor; final Color waveColor; _WaveLoadingPainter({ required this.text, required this.fontSize, required this.animatedValue, required this.backgroundColor, required this.foregroundColor, required this.waveColor, }); @override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); } void _drawText( {required Canvas canvas, required double side, required Color color}) { ParagraphBuilder paragraphBuilder = ParagraphBuilder(ParagraphStyle( textAlign: TextAlign.center, fontStyle: FontStyle.normal, fontSize: fontSize, )); paragraphBuilder.pushStyle(ui.TextStyle(color: color)); paragraphBuilder.addText(text); ParagraphConstraints pc = ParagraphConstraints(width: fontSize); Paragraph paragraph = paragraphBuilder.build()..layout(pc); canvas.drawParagraph( paragraph, Offset((side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0), ); } @override bool shouldRepaint(CustomPainter oldDelegate) { return animatedValue != (oldDelegate as _WaveLoadingPainter).animatedValue; } }
取 widget 的宽度和高度的最小值作为圆的直径大小,以此构建出一个不超出 widget 范围的最大圆形路径 circlePath
@override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); final circlePath = Path(); circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); }
波浪的宽度和高度就根据一个固定的比例值来求值,以 circlePath 的中间分隔线作为水平线,在水平线的上下根据贝塞尔曲线绘制出连续的波浪线
@override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); final circlePath = Path(); circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); final waveWidth = side * 0.8; final waveHeight = side / 6; final wavePath = Path(); final radius = side / 2.0; 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); } //为了方便读者理解,这里把 wavePath 绘制出来,实际上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint); }
此时绘制的曲线还处于非闭合状态,需要将 wavePath 的首尾两端连接起来,这样后面才可以和 circlePath 取交集
wavePath.relativeLineTo(0, radius); wavePath.lineTo(-waveWidth, side); wavePath.close(); //为了方便读者理解,这里把 wavePath 绘制出来,实际上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint);
wavePath 闭合后,此时半圆的颜色就会铺满了
取 circlePath 和 wavePath 的交集,就得到一个半圆形波浪球了
final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; final combinePath = Path.combine(PathOperation.intersect, circlePath, wavePath); canvas.drawPath(combinePath, paint);
文本的颜色是分为上下两部分的,上半部分颜色为 backgroundColor,下半部分为 foregroundColor。在第一步的时候已经绘制了颜色为 backgroundColor 的文本了,foregroundColor 文本不需要显示上半部分,所以在绘制 foregroundColor 文本之前需要先把绘制区域限定在 combinePath 内,使得两次不同时间绘制的文本重叠在了一起,从而得到有不同颜色范围的文本
canvas.clipPath(combinePath); _drawText(canvas: canvas, side: side, color: foregroundColor);
现在已经绘制好静态时的效果了,可以考虑如何使 widget 动起来了
要实现动态效果也很简单,只要不断改变贝塞尔曲线的起始点坐标,使之不断从左往右移动,就可以营造出波浪从左往右前进的效果了。_WaveLoadingPainter 根据外部传入的动画值 animatedValue 来设置 wavePath 的起始坐标点即可,生成 animatedValue 的逻辑和其它绘制参数均由 _WaveLoadingState 来提供
class _WaveLoadingState extends State<WaveLoading> with SingleTickerProviderStateMixin { String get _text => widget.text; double get _fontSize => widget.fontSize; Color get _backgroundColor => widget.backgroundColor; Color get _foregroundColor => widget.foregroundColor; Color get _waveColor => widget.waveColor; late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 700), vsync: this); _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 RepaintBoundary( child: CustomPaint( painter: _WaveLoadingPainter( text: _text, fontSize: _fontSize, animatedValue: _animation.value, backgroundColor: _backgroundColor, foregroundColor: _foregroundColor, waveColor: _waveColor, ), ), ); } }
_WaveLoadingPainter 根据 animatedValue 来设置 wavePath 的起始坐标点
wavePath.moveTo((animatedValue - 1) * waveWidth, radius);
最后将 _WaveLoadingState 包裹到 StatefulWidget 中,在 StatefulWidget 中开放可以自定义配置的参数就可以了
class WaveLoading extends StatefulWidget { final String text; final double fontSize; final Color backgroundColor; final Color foregroundColor; final Color waveColor; WaveLoading({ Key? key, required this.text, required this.fontSize, required this.backgroundColor, required this.foregroundColor, required this.waveColor, }) : super(key: key) { assert(text.isNotEmpty && fontSize > 0); } @override State<StatefulWidget> createState() { return _WaveLoadingState(); } }
使用方式:
SizedBox( width: 300, height: 300, child: WaveLoading( text: "開", fontSize: 210, backgroundColor: Colors.lightBlue, foregroundColor: Colors.white, waveColor: Colors.lightBlue, )
源代码看这里:WaveLoadingWidget