Files
tuiche/lib/component/home_page/DynamicReportDetailWidget.dart

1047 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<SleepDateWidget> sleepDateWidgets;
// final List<SleepDataModuleWidget> sleepDataModuleWidgets;
// final Map targetDevice;
// const DynamicReportDetailWidget({
// Key? key,
// required this.sleepDateWidgets,
// required this.sleepDataModuleWidgets,
// required this.targetDevice,
// }) : super(key: key);
// @override
// State<DynamicReportDetailWidget> createState() =>
// _DynamicReportDetailWidgetState();
// }
// class _DynamicReportDetailWidgetState extends State<DynamicReportDetailWidget> {
// 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<SleepDateWidget> 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<SleepDataModuleWidget> 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<WidgetMarquee> {
// 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<SleepDateWidget> sleepDateWidgets;
final List<SleepDataModuleWidget> sleepDataModuleWidgets;
final Map targetDevice;
const DynamicReportDetailWidget({
Key? key,
required this.sleepDateWidgets,
required this.sleepDataModuleWidgets,
required this.targetDevice,
}) : super(key: key);
@override
State<DynamicReportDetailWidget> createState() =>
_DynamicReportDetailWidgetState();
}
class _DynamicReportDetailWidgetState extends State<DynamicReportDetailWidget> {
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<SleepDateWidget> 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<SleepDataModuleWidget> 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<WidgetMarquee> createState() => _WidgetMarqueeState();
// }
// class _WidgetMarqueeState extends State<WidgetMarquee> {
// 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<SleepDataModuleWidget> 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<WidgetMarquee> createState() => _WidgetMarqueeState();
}
class _WidgetMarqueeState extends State<WidgetMarquee> {
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(), // 无缝循环
],
),
),
);
},
);
}
}