auto_scroll_list_view.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import 'dart:async';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/rendering.dart';
  4. import 'package:flutter_screenutil/flutter_screenutil.dart';
  5. class AutoScrollListView extends StatefulWidget {
  6. final NullableIndexedWidgetBuilder itemBuilder;
  7. final int itemCount;
  8. final Axis scrollDirection;
  9. final EdgeInsetsGeometry? padding;
  10. final bool isAutoScrolling;
  11. const AutoScrollListView({
  12. super.key,
  13. required this.itemBuilder,
  14. required this.itemCount,
  15. this.padding,
  16. this.isAutoScrolling = true,
  17. this.scrollDirection = Axis.horizontal,
  18. });
  19. @override
  20. State<AutoScrollListView> createState() => _AutoScrollListViewState();
  21. }
  22. class _AutoScrollListViewState extends State<AutoScrollListView> {
  23. final ScrollController scrollController = ScrollController();
  24. Timer? _timer;
  25. bool _isUserScrolling = true;
  26. @override
  27. void initState() {
  28. super.initState();
  29. _isUserScrolling = widget.isAutoScrolling;
  30. _startAutoScroll();
  31. }
  32. @override
  33. void dispose() {
  34. super.dispose();
  35. scrollController.dispose();
  36. _timer?.cancel();
  37. }
  38. void _startAutoScroll() {
  39. _timer = Timer.periodic(Duration(milliseconds: 50), (timer) {
  40. if (!_isUserScrolling || !scrollController.hasClients) return;
  41. final maxScrollExtent = scrollController.position.maxScrollExtent;
  42. final current = scrollController.position.pixels;
  43. double next = current + 1;
  44. if (next >= maxScrollExtent) {
  45. next = 0;
  46. }
  47. scrollController.jumpTo(next);
  48. });
  49. }
  50. void _onUserScroll() {
  51. if (!_isUserScrolling) {
  52. return;
  53. }
  54. _timer?.cancel();
  55. _timer = Timer(Duration(seconds: 1), () {
  56. setState(() {
  57. _isUserScrolling = widget.isAutoScrolling;
  58. });
  59. _startAutoScroll();
  60. });
  61. }
  62. @override
  63. Widget build(BuildContext context) {
  64. return NotificationListener<UserScrollNotification>(
  65. onNotification: (notification) {
  66. if (notification.direction != ScrollDirection.idle) {
  67. _onUserScroll(); // 用户滑动时暂停自动滚动
  68. }
  69. return false; // 不阻止子组件的事件传递
  70. },
  71. child: ListView.builder(
  72. padding: widget.padding,
  73. scrollDirection: widget.scrollDirection,
  74. itemCount: 1000000,
  75. itemBuilder: (context, index) {
  76. return widget.itemBuilder(context, index % widget.itemCount);
  77. },
  78. controller: scrollController,
  79. ),
  80. );
  81. }
  82. }
  83. // 多行横向自动滚动列表
  84. /// 一个支持自动滚动的多行横向列表,每一行自动向右滚动(模拟跑马灯效果)
  85. class StaggeredAutoScrollListView extends StatefulWidget {
  86. /// 子项构建器(需根据 itemIndex 构建对应组件)
  87. final NullableIndexedWidgetBuilder itemBuilder;
  88. /// 实际数据项数量(不是扩展后的数量)
  89. final int itemCount;
  90. /// 每行的内边距
  91. final EdgeInsetsGeometry? padding;
  92. /// 是否启用自动滚动
  93. final bool isAutoScrolling;
  94. const StaggeredAutoScrollListView({
  95. super.key,
  96. required this.itemBuilder,
  97. required this.itemCount,
  98. this.padding,
  99. this.isAutoScrolling = true,
  100. });
  101. @override
  102. State<StaggeredAutoScrollListView> createState() => _StaggeredAutoScrollListViewState();
  103. }
  104. class _StaggeredAutoScrollListViewState extends State<StaggeredAutoScrollListView> {
  105. final int rowCount = 3; // 列表的总行数
  106. final List<ScrollController> _controllers = []; // 每一行的滚动控制器
  107. final List<Timer?> _timers = []; // 每一行对应的自动滚动定时器
  108. final int loopFactor = 3; // 每行内容扩展的倍数,防止滚动过短
  109. @override
  110. void initState() {
  111. super.initState();
  112. // 初始化每一行的滚动控制器
  113. for (int i = 0; i < rowCount; i++) {
  114. final controller = ScrollController();
  115. _controllers.add(controller);
  116. }
  117. // 如果启用自动滚动,则在渲染后启动滚动逻辑
  118. if (widget.isAutoScrolling) {
  119. WidgetsBinding.instance.addPostFrameCallback((_) {
  120. _startAllScrolls();
  121. });
  122. }
  123. }
  124. @override
  125. void dispose() {
  126. // 销毁滚动控制器
  127. for (final controller in _controllers) {
  128. controller.dispose();
  129. }
  130. // 取消所有定时器
  131. for (final timer in _timers) {
  132. timer?.cancel();
  133. }
  134. super.dispose();
  135. }
  136. /// 启动所有行的自动滚动
  137. void _startAllScrolls() {
  138. for (int i = 0; i < rowCount; i++) {
  139. final controller = _controllers[i];
  140. // 每 50ms 滚动一次,实现平滑向右滚动效果
  141. final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
  142. if (!controller.hasClients) return;
  143. final maxExtent = controller.position.maxScrollExtent;
  144. final current = controller.position.pixels;
  145. final next = current + 1;
  146. if (next >= maxExtent) {
  147. // 若滚动到底部,则跳转到中间,避免突然回到顶部产生视觉跳变
  148. controller.jumpTo(maxExtent / 2);
  149. } else {
  150. controller.jumpTo(next);
  151. }
  152. });
  153. _timers.add(timer);
  154. }
  155. }
  156. @override
  157. Widget build(BuildContext context) {
  158. // 每一行应显示的实际数据项数(取整向上)
  159. final actualItemsPerRow = (widget.itemCount / rowCount).ceil();
  160. // 每一行扩展后的 item 数量(用于循环滚动)
  161. final extendedItemsPerRow = actualItemsPerRow * loopFactor;
  162. return Column(
  163. children: List.generate(rowCount, (rowIndex) {
  164. return SizedBox(
  165. height: 50.w, // 每行高度,单位为屏幕适配宽度
  166. child: ListView.builder(
  167. controller: _controllers[rowIndex], // 设置对应行的滚动控制器
  168. scrollDirection: Axis.horizontal,
  169. padding: widget.padding,
  170. itemCount: extendedItemsPerRow, // 扩展后的数量,制造无限滚动感
  171. itemBuilder: (context, index) {
  172. // 计算实际要展示的数据项索引
  173. final itemIndex = (rowIndex * actualItemsPerRow + index) % widget.itemCount;
  174. return widget.itemBuilder(context, itemIndex);
  175. },
  176. ),
  177. );
  178. }),
  179. );
  180. }
  181. }