经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
Android?贝塞尔曲线绘制一个波浪球
来源:jb51  时间:2022/5/18 8:42:47  对本文有异议

前言

当 flutter 的现有组件无法满足产品要求的 UI 效果时,我们就需要通过自绘组件的方式来进行实现了。本篇文章就来介绍如何用 flutter 自定义实现一个带文本的波浪球,效果如下所示:

先来总结下 WaveLoadingWidget 的特点,这样才能归纳出实现该效果所需要的步骤:

  • widget 的主体是一个不规则的半圆形,顶部曲线以类似于波浪的形式从左往右上下起伏运行
  • 波浪球可以自定义颜色,此处以 waveColor 命名
  • 波浪球的起伏线将嵌入的文本分为上下两种颜色,上半部分颜色以 backgroundColor 命名,下半部分颜色以 foregroundColor 命名,文本的整体颜色一直在根据波浪的运行而动态变化中

虽然文本的整体颜色是在不断变化的,但只要能够绘制出其中一帧的图形,其动态效果就能通过不断改变波浪曲线的位置参数来实现,所以这里先把该 widget 当成静态的,先实现其静态效果即可

将绘制步骤拆解为以下几步:

  • 绘制颜色为 backgroundColor 的文本,将其绘制在 canvas 的最底层
  • 根据 widget 的宽高信息构建一个不超出范围的最大圆形路径 circlePath
  • 以 circlePath 的水平中间线作为波浪的基准起伏线,在起伏线的上边和下边分别用贝塞尔曲线绘制一段连续的波浪 path,将 path 的首尾两端以矩形的方式连接在一起,构成 wavePath,wavePath 的底部会与 circlePath 的最底部相交
  • 取 circlePath 和 wavePath 的交集 combinePath,用 waveColor 填充, 此时就得到了半圆形的球形波浪了
  • 利用 canvas.clipPath(combinePath) 方法裁切画布,再绘制颜色为 foregroundColor 的文本,此时绘制的 foregroundColor 文本只会显示 combinePath 范围内的部分,也即只会显示下半部分,使得两次不同时间绘制的文本重叠在了一起,从而得到了有不同颜色范围的文本
  • 利用 AnimationController 不断改变 wavePath 的起始点的 X 坐标,同时重新刷新 UI,从而得到波浪不断从左往右起伏运行的动态效果

现在就来一步步实现以上的绘制步骤吧

一、绘制 backgroundColor 文本

flutter 通过 CustomPainter 为开发者提供了自绘 UI 的入口,其内部的 void paint(Canvas canvas, Size size) 方法提供了画布 canvas 对象以及包含 widget 宽高信息的 size 对象

这里就来继承 CustomPainter 类,在 paint 方法中先来绘制颜色为 backgroundColor 的文本。flutter 的 canvas 对象没有提供直接 drawText 的 API,所以其绘制文本的步骤相对原生的自定义 View 要稍微麻烦一点

  1. class _WaveLoadingPainter extends CustomPainter {
  2. final String text;
  3.  
  4. final double fontSize;
  5.  
  6. final double animatedValue;
  7.  
  8. final Color backgroundColor;
  9.  
  10. final Color foregroundColor;
  11.  
  12. final Color waveColor;
  13.  
  14. _WaveLoadingPainter({
  15. required this.text,
  16. required this.fontSize,
  17. required this.animatedValue,
  18. required this.backgroundColor,
  19. required this.foregroundColor,
  20. required this.waveColor,
  21. });
  22.  
  23. @override
  24. void paint(Canvas canvas, Size size) {
  25. final side = min(size.width, size.height);
  26. _drawText(canvas: canvas, side: side, color: backgroundColor);
  27. }
  28.  
  29. void _drawText(
  30. {required Canvas canvas, required double side, required Color color}) {
  31. ParagraphBuilder paragraphBuilder = ParagraphBuilder(ParagraphStyle(
  32. textAlign: TextAlign.center,
  33. fontStyle: FontStyle.normal,
  34. fontSize: fontSize,
  35. ));
  36. paragraphBuilder.pushStyle(ui.TextStyle(color: color));
  37. paragraphBuilder.addText(text);
  38. ParagraphConstraints pc = ParagraphConstraints(width: fontSize);
  39. Paragraph paragraph = paragraphBuilder.build()..layout(pc);
  40. canvas.drawParagraph(
  41. paragraph,
  42. Offset((side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0),
  43. );
  44. }
  45.  
  46. @override
  47. bool shouldRepaint(CustomPainter oldDelegate) {
  48. return animatedValue != (oldDelegate as _WaveLoadingPainter).animatedValue;
  49. }
  50. }

二、构建 circlePath

取 widget 的宽度和高度的最小值作为圆的直径大小,以此构建出一个不超出 widget 范围的最大圆形路径 circlePath

  1. @override
  2. void paint(Canvas canvas, Size size) {
  3. final side = min(size.width, size.height);
  4. _drawText(canvas: canvas, side: side, color: backgroundColor);
  5.  
  6. final circlePath = Path();
  7. circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
  8. }

三、绘制波浪线

波浪的宽度和高度就根据一个固定的比例值来求值,以 circlePath 的中间分隔线作为水平线,在水平线的上下根据贝塞尔曲线绘制出连续的波浪线

  1. @override
  2. void paint(Canvas canvas, Size size) {
  3. final side = min(size.width, size.height);
  4. _drawText(canvas: canvas, side: side, color: backgroundColor);
  5.  
  6. final circlePath = Path();
  7. circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
  8.  
  9. final waveWidth = side * 0.8;
  10. final waveHeight = side / 6;
  11. final wavePath = Path();
  12. final radius = side / 2.0;
  13. wavePath.moveTo(-waveWidth, radius);
  14. for (double i = -waveWidth; i < side; i += waveWidth) {
  15. wavePath.relativeQuadraticBezierTo(
  16. waveWidth / 4, -waveHeight, waveWidth / 2, 0);
  17. wavePath.relativeQuadraticBezierTo(
  18. waveWidth / 4, waveHeight, waveWidth / 2, 0);
  19. }
  20. //为了方便读者理解,这里把 wavePath 绘制出来,实际上不需要
  21. final paint = Paint()
  22. ..isAntiAlias = true
  23. ..style = PaintingStyle.fill
  24. ..strokeWidth = 3
  25. ..color = waveColor;
  26. canvas.drawPath(wavePath, paint);
  27. }

此时绘制的曲线还处于非闭合状态,需要将 wavePath 的首尾两端连接起来,这样后面才可以和 circlePath 取交集

  1. wavePath.relativeLineTo(0, radius);
  2. wavePath.lineTo(-waveWidth, side);
  3. wavePath.close();
  4. //为了方便读者理解,这里把 wavePath 绘制出来,实际上不需要
  5. final paint = Paint()
  6. ..isAntiAlias = true
  7. ..style = PaintingStyle.fill
  8. ..strokeWidth = 3
  9. ..color = waveColor;
  10. canvas.drawPath(wavePath, paint);

wavePath 闭合后,此时半圆的颜色就会铺满了

四、取交集

取 circlePath 和 wavePath 的交集,就得到一个半圆形波浪球了

  1. final paint = Paint()
  2. ..isAntiAlias = true
  3. ..style = PaintingStyle.fill
  4. ..strokeWidth = 3
  5. ..color = waveColor;
  6. final combinePath = Path.combine(PathOperation.intersect, circlePath, wavePath);
  7. canvas.drawPath(combinePath, paint);

五、绘制 foregroundColor 文本

文本的颜色是分为上下两部分的,上半部分颜色为 backgroundColor,下半部分为 foregroundColor。在第一步的时候已经绘制了颜色为 backgroundColor 的文本了,foregroundColor 文本不需要显示上半部分,所以在绘制 foregroundColor 文本之前需要先把绘制区域限定在 combinePath 内,使得两次不同时间绘制的文本重叠在了一起,从而得到有不同颜色范围的文本

  1. canvas.clipPath(combinePath);
  2. _drawText(canvas: canvas, side: side, color: foregroundColor);

六、添加动画

现在已经绘制好静态时的效果了,可以考虑如何使 widget 动起来了

要实现动态效果也很简单,只要不断改变贝塞尔曲线的起始点坐标,使之不断从左往右移动,就可以营造出波浪从左往右前进的效果了。_WaveLoadingPainter 根据外部传入的动画值 animatedValue 来设置 wavePath 的起始坐标点即可,生成 animatedValue 的逻辑和其它绘制参数均由 _WaveLoadingState 来提供

  1. class _WaveLoadingState extends State<WaveLoading>
  2. with SingleTickerProviderStateMixin {
  3. String get _text => widget.text;
  4.  
  5. double get _fontSize => widget.fontSize;
  6.  
  7. Color get _backgroundColor => widget.backgroundColor;
  8.  
  9. Color get _foregroundColor => widget.foregroundColor;
  10.  
  11. Color get _waveColor => widget.waveColor;
  12.  
  13. late AnimationController _controller;
  14.  
  15. late Animation<double> _animation;
  16.  
  17. @override
  18. void initState() {
  19. super.initState();
  20. _controller = AnimationController(
  21. duration: const Duration(milliseconds: 700), vsync: this);
  22. _animation = Tween(
  23. begin: 0.0,
  24. end: 1.0,
  25. ).animate(_controller)
  26. ..addListener(() {
  27. setState(() => {});
  28. });
  29. _controller.repeat();
  30. }
  31.  
  32. @override
  33. void dispose() {
  34. _controller.dispose();
  35. super.dispose();
  36. }
  37.  
  38. @override
  39. Widget build(BuildContext context) {
  40. return RepaintBoundary(
  41. child: CustomPaint(
  42. painter: _WaveLoadingPainter(
  43. text: _text,
  44. fontSize: _fontSize,
  45. animatedValue: _animation.value,
  46. backgroundColor: _backgroundColor,
  47. foregroundColor: _foregroundColor,
  48. waveColor: _waveColor,
  49. ),
  50. ),
  51. );
  52. }
  53. }

_WaveLoadingPainter 根据 animatedValue 来设置 wavePath 的起始坐标点

  1. wavePath.moveTo((animatedValue - 1) * waveWidth, radius);

七、使用

最后将 _WaveLoadingState 包裹到 StatefulWidget 中,在 StatefulWidget 中开放可以自定义配置的参数就可以了

  1. class WaveLoading extends StatefulWidget {
  2. final String text;
  3.  
  4. final double fontSize;
  5.  
  6. final Color backgroundColor;
  7.  
  8. final Color foregroundColor;
  9.  
  10. final Color waveColor;
  11.  
  12. WaveLoading({
  13. Key? key,
  14. required this.text,
  15. required this.fontSize,
  16. required this.backgroundColor,
  17. required this.foregroundColor,
  18. required this.waveColor,
  19. }) : super(key: key) {
  20. assert(text.isNotEmpty && fontSize > 0);
  21. }
  22.  
  23. @override
  24. State<StatefulWidget> createState() {
  25. return _WaveLoadingState();
  26. }
  27. }

使用方式:

  1. SizedBox(
  2. width: 300,
  3. height: 300,
  4. child: WaveLoading(
  5. text: "開",
  6. fontSize: 210,
  7. backgroundColor: Colors.lightBlue,
  8. foregroundColor: Colors.white,
  9. waveColor: Colors.lightBlue,
  10. )

源代码看这里:WaveLoadingWidget

以上就是Android 贝塞尔曲线绘制一个波浪球的详细内容,更多关于Android贝塞尔曲线的资料请关注w3xue其它相关文章!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号