animated_progress_bar.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import 'package:flutter/material.dart';
  2. import 'bubble/bubble_widget.dart';
  3. /// 支持动画过渡和渐变色的进度条
  4. // 使用示例
  5. // AnimatedGradientProgressBar(
  6. // targetValue: 0.75,
  7. // gradient: LinearGradient(colors: [Colors.orange, Colors.red]),
  8. // height: 10,
  9. // borderRadius: 5,
  10. // )
  11. class AnimatedGradientProgressBar extends StatefulWidget {
  12. // 目标进度值(0~1)
  13. final double targetValue;
  14. // 动画时长
  15. final Duration duration;
  16. // 渐变色
  17. final Gradient gradient;
  18. // 进度条高度
  19. final double height;
  20. // 圆角半径
  21. final double borderRadius;
  22. const AnimatedGradientProgressBar({
  23. super.key,
  24. required this.targetValue,
  25. required this.duration,
  26. required this.gradient,
  27. this.height = 10.0,
  28. this.borderRadius = 7.0,
  29. });
  30. @override
  31. AnimatedGradientProgressBarState createState() =>
  32. AnimatedGradientProgressBarState();
  33. }
  34. class AnimatedGradientProgressBarState
  35. extends State<AnimatedGradientProgressBar>
  36. with TickerProviderStateMixin {
  37. late AnimationController _controller;
  38. late Animation<double> _animation;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _controller = AnimationController(duration: widget.duration, vsync: this);
  43. _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
  44. _startAnimation();
  45. }
  46. @override
  47. void didUpdateWidget(AnimatedGradientProgressBar oldWidget) {
  48. super.didUpdateWidget(oldWidget);
  49. if (oldWidget.targetValue != widget.targetValue) {
  50. _resetAnimation();
  51. _startAnimation();
  52. }
  53. }
  54. @override
  55. void dispose() {
  56. // 移除监听
  57. _animation.removeListener(() {});
  58. _controller.dispose();
  59. super.dispose();
  60. }
  61. void _startAnimation() {
  62. print('进度条 => 初始动画值: ${_animation.value}'); // 0.0
  63. // 先移除旧监听
  64. _animation.removeListener(() {});
  65. // 创建新的 Tween 动画
  66. _animation = Tween<double>(
  67. begin: 0,
  68. end: widget.targetValue,
  69. ).animate(_animation);
  70. // 添加监听器并启动动画
  71. _animation.addListener(() => setState(() {}));
  72. _controller.forward();
  73. Future.delayed(widget.duration, () {
  74. print('进度条 => 最终动画值: ${_animation.value}');
  75. });
  76. }
  77. void _resetAnimation() => _controller.reset();
  78. @override
  79. Widget build(BuildContext context) {
  80. return LayoutBuilder(
  81. builder: (context, constraints) {
  82. // 进度条的实际宽度
  83. final progressBarWidth = constraints.maxWidth;
  84. // 计算气泡距离左边的偏移量
  85. double leftOffset = 0;
  86. if (_animation.value == 0) {
  87. leftOffset = 0;
  88. } else {
  89. leftOffset =
  90. _animation.value * progressBarWidth -
  91. (widget.targetValue > 0.5 ? 32 : 24);
  92. }
  93. return Stack(
  94. // 允许子组件溢出自己本身的大小,默认是裁切的
  95. clipBehavior: Clip.none,
  96. children: [
  97. // 百分比文本
  98. Positioned(
  99. // 左侧偏移量,气泡跟随在当前进度的末尾,公式:当前比例值 * 进度条的宽度 - 气泡宽度
  100. left: leftOffset,
  101. // 进度气泡,位于进度条上方
  102. bottom: widget.height + 0.85,
  103. child: AnimatedBuilder(
  104. animation: _animation,
  105. builder: (context, _) {
  106. return BubbleWidget(
  107. // 箭头方向
  108. arrowDirection: AxisDirection.down,
  109. // 箭头距离左边的偏移量
  110. arrowOffset: leftOffset == 0 ? 10 : 22,
  111. arrowLength: 8,
  112. arrowRadius: 2,
  113. arrowWidth: 5,
  114. padding: const EdgeInsets.symmetric(
  115. vertical: 2,
  116. horizontal: 7,
  117. ),
  118. borderRadius: BorderRadius.circular(9),
  119. // 气泡的背景颜色,取渐变色的第2个颜色
  120. backgroundColor: widget.gradient.colors.last,
  121. contentBuilder: (context) {
  122. return Text(
  123. '${(_animation.value * 100).toStringAsFixed(0)}%',
  124. style: const TextStyle(
  125. fontSize: 10,
  126. color: Colors.white,
  127. ),
  128. );
  129. },
  130. );
  131. },
  132. ),
  133. ),
  134. // 进度条
  135. ClipRRect(
  136. borderRadius: BorderRadius.circular(widget.borderRadius),
  137. child: Container(
  138. width: double.infinity,
  139. height: widget.height,
  140. decoration: BoxDecoration(
  141. color: Colors.grey[200],
  142. borderRadius: BorderRadius.circular(widget.borderRadius),
  143. ),
  144. child: Stack(
  145. children: [
  146. AnimatedBuilder(
  147. animation: _animation,
  148. builder: (context, _) {
  149. return FractionallySizedBox(
  150. widthFactor: _animation.value,
  151. alignment: Alignment.centerLeft,
  152. child: Container(
  153. decoration: BoxDecoration(
  154. gradient: widget.gradient,
  155. borderRadius: BorderRadius.circular(
  156. widget.borderRadius,
  157. ),
  158. ),
  159. ),
  160. );
  161. },
  162. ),
  163. ],
  164. ),
  165. ),
  166. ),
  167. ],
  168. );
  169. },
  170. );
  171. }
  172. }