// import 'dart:async'; // import 'package:ef/ef.dart'; // import 'package:flutter/material.dart'; // import 'package:flutter_svg/svg.dart'; // import 'package:flutterflow_ui/flutterflow_ui.dart'; // import 'package:vbvs_app/common/color/appConstants.dart'; // import 'package:vbvs_app/common/util/FitTool.dart'; // import 'package:vbvs_app/common/util/MyUtils.dart'; // import 'package:vbvs_app/component/home_page/SleepDataModuleWidget.dart'; // import 'package:vbvs_app/component/home_page/SleepDateWidget.dart'; // import 'package:vbvs_app/component/tool/ClickableContainer.dart'; // import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; // import 'package:vbvs_app/controller/device/body_device_controller.dart'; // import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; // import 'package:vbvs_app/enum/DataStatus.dart'; // class DynamicReportDetailWidget extends StatefulWidget { // final List sleepDateWidgets; // final List sleepDataModuleWidgets; // final Map targetDevice; // const DynamicReportDetailWidget({ // Key? key, // required this.sleepDateWidgets, // required this.sleepDataModuleWidgets, // required this.targetDevice, // }) : super(key: key); // @override // State createState() => // _DynamicReportDetailWidgetState(); // } // class _DynamicReportDetailWidgetState extends State { // final ThemeController themeController = Get.find(); // final ScrollController _scrollController = ScrollController(); // bool _hasScrolled = false; // BodyDeviceController bodyDeviceController = Get.find(); // @override // void initState() { // super.initState(); // WidgetsBinding.instance.addPostFrameCallback((_) { // Future.delayed(Duration(milliseconds: 1000), () { // if (!_hasScrolled && _scrollController.hasClients // // && _scrollController.position.maxScrollExtent > 0 // ) { // _scrollController.animateTo( // _scrollController.position.maxScrollExtent, // duration: Duration(milliseconds: 300), // curve: Curves.easeOut, // ); // _hasScrolled = true; // } // }); // }); // } // @override // Widget build(BuildContext context) { // return Padding( // padding: EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0, 0.rpx), // child: Container( // width: double.infinity, // decoration: BoxDecoration( // color: themeController.currentColor.sc5, // borderRadius: // BorderRadius.circular(AppConstants().normal_container_radius), // ), // child: Padding( // padding: // EdgeInsetsDirectional.fromSTEB(30.rpx, 30.rpx, 30.rpx, 30.rpx), // child: Column( // mainAxisSize: MainAxisSize.max, // children: [ // _buildHeader(context, widget.targetDevice), // SizedBox(height: 33.rpx), // _buildSleepDateWidgets(), // SizedBox(height: 20.rpx), // if (!AppConstants.is_test_account) // Obx(() { // return _buildSleepDataModuleWidgets(); // }), // ], // ), // ), // ), // ); // } // Widget _buildHeader(BuildContext context, Map targetDevice) { // return Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // ClickableContainer( // backgroundColor: Colors.transparent, // highlightColor: themeController.currentColor.sc3.withOpacity(0.2), // borderRadius: 0, // padding: EdgeInsets.zero, // onTap: () async { // await Get.toNamed("/bodyDevice", arguments: targetDevice); // }, // child: Container( // constraints: BoxConstraints( // maxWidth: MediaQuery.sizeOf(context).width * 0.5, // ), // child: Text( // '${targetDevice['person']?['name'] == null || targetDevice['person']?['name'].isEmpty ? '体征监测设备'.tr : targetDevice['person']['name']}', // style: TextStyle( // fontFamily: 'Inter', // fontSize: 30.rpx, // letterSpacing: 0.0, // color: themeController.currentColor.sc3, // ), // maxLines: 1, // overflow: TextOverflow.ellipsis, // ), // ), // ), // if (!AppConstants.is_test_account) // ClickableContainer( // backgroundColor: Colors.transparent, // highlightColor: themeController.currentColor.sc3, // borderRadius: 0, // padding: EdgeInsets.zero, // onTap: () { // String mac = targetDevice['mac']; // List selectedWidgets = widget.sleepDateWidgets // .where((w) => w.isSelected == true) // .toList(); // if (selectedWidgets.isNotEmpty) { // DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( // int.parse(selectedWidgets[0].time!)); // String time = MyUtils.formatBindTime(dateTime); // // String sleepReportUrl = // // "${ServiceConstant.sleep_report_url}?mac=$mac&token=${ServiceConstant.sleep_token}&date=$time"; // // Get.toNamed("/sleepReportPage", arguments: sleepReportUrl); // Get.toNamed("/newSleepReportPage", arguments: { // 'date': dateTime != null // ? dateTime.millisecondsSinceEpoch // : DateTime.now().millisecondsSinceEpoch, // "mac": mac, // 'type': 1, // 'name': 'sleep', //'sleep', 'heartRate' 或 'breathe' // // 'itemName': widget.data['id'], // 'person': widget.targetDevice['person'], // }); // } else { // TopSlideNotification.show(context, // text: "当前暂无数据".tr, // textColor: themeController.currentColor.sc9); // } // }, // child: Row( // children: [ // Text( // '首页.报告详情'.tr, // style: TextStyle( // fontFamily: 'Inter', // fontSize: 26.rpx, // letterSpacing: 0.0, // color: themeController.currentColor.sc2, // ), // ), // Padding( // padding: EdgeInsetsDirectional.fromSTEB(0, 6.rpx, 0, 0.rpx), // child: SvgPicture.asset( // 'assets/img/icon/arrow_right.svg', // width: 14.rpx, // height: 14.rpx, // color: themeController.currentColor.sc3, // ), // ), // ].divide(SizedBox(width: 22.rpx)), // ), // ), // ], // ); // } // Widget _buildSleepDateWidgets() { // return Container( // width: double.infinity, // child: SingleChildScrollView( // controller: _scrollController, // scrollDirection: Axis.horizontal, // child: Row( // children: widget.sleepDateWidgets // .map((widget) => widget) // .toList() // .divide(SizedBox(width: 20.rpx)), // ), // ), // ); // } // // Widget _buildSleepDataModuleWidgets() { // // // homePageSleepFlag // // //widget.targetDevice['mac'] // // if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == // // DataStatus.Loading.code) { // // return Container( // // height: 200.rpx, // // alignment: Alignment.center, // // child: CircularProgressIndicator( // // color: themeController.currentColor.sc1, // // ), // // ); // // } // // if (widget.sleepDataModuleWidgets.isEmpty) { // // return Container( // // height: 200.rpx, // // alignment: Alignment.center, // // child: Text( // // '暂无数据'.tr, // // style: TextStyle( // // fontFamily: 'Inter', // // fontSize: 28.rpx, // // color: themeController.currentColor.sc4, // // ), // // ), // // ); // // } // // // if (widget.sleepDataModuleWidgets.isEmpty) { // // // return Container( // // // height: 200.rpx, // // // alignment: Alignment.center, // // // child: CircularProgressIndicator( // // // color: themeController.currentColor.sc1, // // // ), // // // ); // // // } // // // return Container( // // // width: double.infinity, // // // height: 200.rpx, // // // child: SingleChildScrollView( // // // scrollDirection: Axis.horizontal, // // // child: Row( // // // children: widget.sleepDataModuleWidgets // // // .map((widget) => widget) // // // .toList() // // // .divide(SizedBox(width: 14.rpx)), // // // ), // // // ), // // // ); // // var aa = widget.sleepDataModuleWidgets // // // 过滤:当 data 中存在 'show' 且其为 false 时排除该元素 // // .where((item) => item.data?['show'] != false) // // // 保持元素本身(SleepDataModuleWidget) // // .map((item) => item) // // .toList(); // // return Container( // // width: double.infinity, // // height: 200.rpx, // // child: SingleChildScrollView( // // scrollDirection: Axis.horizontal, // // child: Row( // // children: // // // 保留你原来的 divide 间隔处理 // // aa.divide(SizedBox(width: 14.rpx)), // // ), // // ), // // ); // // } // Widget _buildSleepDataModuleWidgets() { // if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == // DataStatus.Loading.code) { // return Container( // height: 200.rpx, // alignment: Alignment.center, // child: CircularProgressIndicator( // color: themeController.currentColor.sc1, // ), // ); // } // var items = widget.sleepDataModuleWidgets // .where((item) => item.data?['show'] != false) // .toList(); // if (items.isEmpty) { // return Container( // height: 200.rpx, // alignment: Alignment.center, // child: Text( // '暂无数据'.tr, // style: TextStyle( // fontFamily: 'Inter', // fontSize: 28.rpx, // color: themeController.currentColor.sc4, // ), // ), // ); // } // return Container( // height: 200.rpx, // child: WidgetMarquee( // items: items, // itemWidth: 240.rpx, // spacing: 14.rpx, // speed: 30.0, // ), // ); // } // void resetScroll() { // _hasScrolled = false; // if (_scrollController.hasClients) { // _scrollController.jumpTo(0); // } // } // } // class WidgetMarquee extends StatefulWidget { // final List items; // final double itemWidth; // final double spacing; // final double speed; // 像素/秒 // const WidgetMarquee({ // Key? key, // required this.items, // required this.itemWidth, // this.spacing = 14, // this.speed = 30, // }) : super(key: key); // @override // _WidgetMarqueeState createState() => _WidgetMarqueeState(); // } // class _WidgetMarqueeState extends State { // final ScrollController _scrollController = ScrollController(); // Timer? _timer; // double _scrollOffset = 0; // bool _needsMarquee = false; // double _containerWidth = 0; // double _totalWidth = 0; // @override // void initState() { // super.initState(); // WidgetsBinding.instance.addPostFrameCallback((_) { // _checkIfNeedsMarquee(); // _startMarquee(); // }); // } // void _checkIfNeedsMarquee() { // if (!mounted) return; // final containerWidth = context.size?.width ?? 0; // _totalWidth = widget.items.length * widget.itemWidth + // (widget.items.length - 1) * widget.spacing; // setState(() { // _containerWidth = containerWidth; // _needsMarquee = _totalWidth > containerWidth; // }); // } // void _startMarquee() { // if (!_needsMarquee) return; // // 延迟开始滚动,让用户有时间查看初始内容 // Future.delayed(Duration(seconds: 0), () { // if (!mounted) return; // _timer = Timer.periodic(Duration(milliseconds: 16), (timer) { // if (!mounted || !_scrollController.hasClients) { // timer.cancel(); // return; // } // setState(() { // _scrollOffset += widget.speed / 60; // 每帧移动的距离 // // 当滚动到第一组内容的末尾时,重置偏移量 // if (_scrollOffset >= _totalWidth) { // _scrollOffset -= _totalWidth; // } // _scrollController.jumpTo(_scrollOffset); // }); // }); // }); // } // @override // void dispose() { // _timer?.cancel(); // _scrollController.dispose(); // super.dispose(); // } // @override // Widget build(BuildContext context) { // if (widget.items.isEmpty) { // return Container(); // } // if (!_needsMarquee) { // // 不需要滚动,显示单行 // return SingleChildScrollView( // scrollDirection: Axis.horizontal, // child: Row( // children: widget.items // .asMap() // .entries // .map((entry) => Padding( // padding: EdgeInsets.only( // right: entry.key < widget.items.length - 1 // ? widget.spacing // : 0, // ), // child: Container( // width: widget.itemWidth, // child: entry.value, // ), // )) // .toList(), // ), // ); // } // // 需要滚动,使用两倍内容实现无缝滚动 // return SingleChildScrollView( // controller: _scrollController, // scrollDirection: Axis.horizontal, // physics: const NeverScrollableScrollPhysics(), // child: Row( // children: [ // // 第一组 // Row( // children: widget.items // .asMap() // .entries // .map((entry) => Padding( // padding: EdgeInsets.only( // right: entry.key < widget.items.length - 1 // ? widget.spacing // : 0, // ), // child: Container( // width: widget.itemWidth, // child: entry.value, // ), // )) // .toList(), // ), // Row( // children: widget.items // .asMap() // .entries // .map((entry) => Padding( // padding: EdgeInsets.only( // right: entry.key < widget.items.length - 1 // ? widget.spacing // : 0, // ), // child: Container( // width: widget.itemWidth, // child: entry.value, // ), // )) // .toList(), // ), // ], // ), // ); // } // } import 'dart:async'; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/color/appConstants.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; import 'package:vbvs_app/component/home_page/SleepDataModuleWidget.dart'; import 'package:vbvs_app/component/home_page/SleepDateWidget.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/component/tool/TopSlideNotification.dart'; import 'package:vbvs_app/controller/device/body_device_controller.dart'; import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/enum/DataStatus.dart'; class DynamicReportDetailWidget extends StatefulWidget { final List sleepDateWidgets; final List sleepDataModuleWidgets; final Map targetDevice; const DynamicReportDetailWidget({ Key? key, required this.sleepDateWidgets, required this.sleepDataModuleWidgets, required this.targetDevice, }) : super(key: key); @override State createState() => _DynamicReportDetailWidgetState(); } class _DynamicReportDetailWidgetState extends State { final ThemeController themeController = Get.find(); final ScrollController _scrollController = ScrollController(); bool _hasScrolled = false; BodyDeviceController bodyDeviceController = Get.find(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { Future.delayed(const Duration(milliseconds: 1000), () { if (!_hasScrolled && _scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); _hasScrolled = true; } }); }); } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 0.rpx, 0, 0.rpx), child: Container( decoration: BoxDecoration( color: themeController.currentColor.sc5, borderRadius: BorderRadius.circular(AppConstants().normal_container_radius), ), child: Padding( padding: EdgeInsetsDirectional.fromSTEB(30.rpx, 30.rpx, 30.rpx, 30.rpx), child: Column( children: [ _buildHeader(context, widget.targetDevice), SizedBox(height: 33.rpx), _buildSleepDateWidgets(), SizedBox(height: 20.rpx), if (!AppConstants.is_test_account) Obx(() => _buildSleepDataModuleWidgets()), ], ), ), ), ); } Widget _buildHeader(BuildContext context, Map targetDevice) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: ClickableContainer( backgroundColor: Colors.transparent, highlightColor: themeController.currentColor.sc3.withOpacity(0.2), padding: EdgeInsets.zero, onTap: () async { await Get.toNamed("/bodyDevice", arguments: targetDevice); }, child: Text( targetDevice['person']?['name']?.isNotEmpty == true ? targetDevice['person']['name'] : '体征监测设备'.tr, style: TextStyle( fontSize: 30.rpx, color: themeController.currentColor.sc3, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), if (!AppConstants.is_test_account) ClickableContainer( backgroundColor: Colors.transparent, highlightColor: themeController.currentColor.sc3, borderRadius: 0, padding: EdgeInsets.zero, onTap: () { String mac = targetDevice['mac']; List selectedWidgets = widget.sleepDateWidgets .where((w) => w.isSelected == true) .toList(); if (selectedWidgets.isNotEmpty) { DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( int.parse(selectedWidgets[0].time!)); String time = MyUtils.formatBindTime(dateTime); // String sleepReportUrl = // "${ServiceConstant.sleep_report_url}?mac=$mac&token=${ServiceConstant.sleep_token}&date=$time"; // Get.toNamed("/sleepReportPage", arguments: sleepReportUrl); Get.toNamed("/newSleepReportPage", arguments: { 'date': dateTime != null ? dateTime.millisecondsSinceEpoch : DateTime.now().millisecondsSinceEpoch, "mac": mac, 'type': 1, 'name': 'sleep', //'sleep', 'heartRate' 或 'breathe' // 'itemName': widget.data['id'], 'person': widget.targetDevice['person'], }); } else { TopSlideNotification.show(context, text: "当前暂无数据".tr, textColor: themeController.currentColor.sc9); } }, child: Row( children: [ Text( '首页.报告详情'.tr, style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController.currentColor.sc2, ), ), Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 6.rpx, 0, 0.rpx), child: SvgPicture.asset( 'assets/img/icon/arrow_right.svg', width: 14.rpx, height: 14.rpx, color: themeController.currentColor.sc3, ), ), ].divide(SizedBox(width: 22.rpx)), ), ), ], ); } Widget _buildSleepDateWidgets() { return SingleChildScrollView( controller: _scrollController, scrollDirection: Axis.horizontal, child: Row( children: widget.sleepDateWidgets .map((e) => e) .toList() .divide(SizedBox(width: 20.rpx)), ), ); } Widget _buildSleepDataModuleWidgets() { if (bodyDeviceController.homePageSleepFlag[widget.targetDevice['mac']] == DataStatus.Loading.code) { return SizedBox( height: 200.rpx, child: Center( child: CircularProgressIndicator( strokeWidth: 2, color: themeController.currentColor.sc1, ), ), ); } final items = widget.sleepDataModuleWidgets .where((e) => e.data?['show'] != false) .toList(); if (items.isEmpty) { return SizedBox( height: 200.rpx, child: Center( child: Text( '暂无数据'.tr, style: TextStyle( fontSize: 28.rpx, color: themeController.currentColor.sc4, ), ), ), ); } return SizedBox( height: 200.rpx, child: WidgetMarquee( items: items, itemWidth: 240.rpx, spacing: 14.rpx, speed: 30, ), ); } } /* -------------------------------------------------------------------------- */ /* WidgetMarquee */ /* -------------------------------------------------------------------------- */ // class WidgetMarquee extends StatefulWidget { // final List items; // final double itemWidth; // final double spacing; // final double speed; // const WidgetMarquee({ // Key? key, // required this.items, // required this.itemWidth, // this.spacing = 14, // this.speed = 30, // }) : super(key: key); // @override // State createState() => _WidgetMarqueeState(); // } // class _WidgetMarqueeState extends State { // final ScrollController _scrollController = ScrollController(); // Timer? _timer; // Timer? _resumeTimer; // double _scrollOffset = 0; // double _totalWidth = 0; // bool _needsMarquee = false; // bool _isUserInteracting = false; // @override // void initState() { // super.initState(); // WidgetsBinding.instance.addPostFrameCallback((_) { // _calc(); // _start(); // }); // } // void _calc() { // final containerWidth = context.size?.width ?? 0; // _totalWidth = widget.items.length * widget.itemWidth + // (widget.items.length - 1) * widget.spacing; // _needsMarquee = _totalWidth > containerWidth; // } // /// ⭐ 核心:同步真实滚动位置,防止跳帧 // void _syncScrollOffset() { // if (!_scrollController.hasClients) return; // double current = _scrollController.position.pixels; // if (current >= _totalWidth) { // current -= _totalWidth; // _scrollController.jumpTo(current); // } // _scrollOffset = current; // } // void _start() { // if (!_needsMarquee || _timer != null) return; // _timer = Timer.periodic(const Duration(milliseconds: 16), (_) { // if (!_scrollController.hasClients || _isUserInteracting) return; // _scrollOffset += widget.speed / 60; // if (_scrollOffset >= _totalWidth) { // _scrollOffset -= _totalWidth; // } // _scrollController.jumpTo(_scrollOffset); // }); // } // void _stop() { // _timer?.cancel(); // _timer = null; // } // void _resumeLater() { // _resumeTimer?.cancel(); // _resumeTimer = Timer(const Duration(seconds: 2), _start); // } // Widget _buildRow() { // return Row( // children: widget.items // .asMap() // .entries // .map((e) => Padding( // padding: EdgeInsets.only( // right: e.key < widget.items.length - 1 // ? widget.spacing // : 0, // ), // child: SizedBox( // width: widget.itemWidth, // child: e.value, // ), // )) // .toList(), // ); // } // @override // void dispose() { // _stop(); // _resumeTimer?.cancel(); // _scrollController.dispose(); // super.dispose(); // } // @override // Widget build(BuildContext context) { // if (!_needsMarquee) { // return SingleChildScrollView( // scrollDirection: Axis.horizontal, // child: _buildRow(), // ); // } // return Listener( // onPointerDown: (_) { // _isUserInteracting = true; // _stop(); // }, // onPointerUp: (_) { // _isUserInteracting = false; // _syncScrollOffset(); // ⭐ 防跳帧关键 // _resumeLater(); // }, // onPointerCancel: (_) { // _isUserInteracting = false; // _syncScrollOffset(); // _resumeLater(); // }, // child: SingleChildScrollView( // controller: _scrollController, // scrollDirection: Axis.horizontal, // physics: const BouncingScrollPhysics(), // child: Row( // children: [ // _buildRow(), // _buildRow(), // 第二份用于无缝循环 // ], // ), // ), // ); // } // } class WidgetMarquee extends StatefulWidget { final List items; final double itemWidth; final double spacing; final double speed; // px / second const WidgetMarquee({ Key? key, required this.items, required this.itemWidth, this.spacing = 14, this.speed = 30, }) : super(key: key); @override State createState() => _WidgetMarqueeState(); } class _WidgetMarqueeState extends State { final ScrollController _scrollController = ScrollController(); Timer? _timer; Timer? _resumeTimer; double _scrollOffset = 0; double _totalWidth = 0; bool _needsMarquee = false; bool _isUserInteracting = false; // ====================== // 自动滚动控制 // ====================== void _startMarquee() { if (_timer != null || !_needsMarquee) return; _timer = Timer.periodic(const Duration(milliseconds: 16), (_) { if (!_scrollController.hasClients || _isUserInteracting) return; _scrollOffset += widget.speed / 60; if (_scrollOffset >= _totalWidth) { _scrollOffset -= _totalWidth; } _scrollController.jumpTo(_scrollOffset); }); } void _stopMarquee() { _timer?.cancel(); _timer = null; } void _resumeMarqueeWithDelay() { _resumeTimer?.cancel(); _resumeTimer = Timer(const Duration(seconds: 1), () { if (!mounted) return; _startMarquee(); }); } // ====================== // 关键:同步真实滚动位置 // ====================== void _syncScrollOffset() { if (!_scrollController.hasClients) return; double current = _scrollController.position.pixels; if (current >= _totalWidth) { current -= _totalWidth; _scrollController.jumpTo(current); } _scrollOffset = current; } // ====================== // UI // ====================== Widget _buildRow() { return Row( children: widget.items .asMap() .entries .map( (entry) => Padding( padding: EdgeInsets.only( right: entry.key < widget.items.length - 1 ? widget.spacing : 0, ), child: SizedBox( width: widget.itemWidth, child: entry.value, ), ), ) .toList(), ); } @override void dispose() { _timer?.cancel(); _resumeTimer?.cancel(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (widget.items.isEmpty) { return const SizedBox.shrink(); } return LayoutBuilder( builder: (context, constraints) { final containerWidth = constraints.maxWidth; _totalWidth = widget.items.length * widget.itemWidth + (widget.items.length - 1) * widget.spacing; final needsMarquee = _totalWidth > containerWidth; /// ⭐ 第一次满足条件时立即启动自动滚动 if (needsMarquee && !_needsMarquee) { _needsMarquee = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _startMarquee(); }); } /// 不需要滚动 if (!needsMarquee) { _needsMarquee = false; _stopMarquee(); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: _buildRow(), ); } /// 需要滚动(支持手势) return Listener( onPointerDown: (_) { _isUserInteracting = true; _stopMarquee(); }, onPointerUp: (_) { _isUserInteracting = false; _syncScrollOffset(); _resumeMarqueeWithDelay(); }, onPointerCancel: (_) { _isUserInteracting = false; _syncScrollOffset(); _resumeMarqueeWithDelay(); }, child: SingleChildScrollView( controller: _scrollController, scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( children: [ _buildRow(), SizedBox( width: 20.rpx, ), _buildRow(), // 无缝循环 ], ), ), ); }, ); } }