// import 'dart:ui' as ui; // import 'package:flutter/material.dart'; // import 'package:flutterflow_ui/flutterflow_ui.dart'; // import 'package:vbvs_app/common/util/FitTool.dart'; // import 'package:vbvs_app/common/util/MyUtils.dart'; // import 'package:intl/intl.dart'; // class SnoreChartContainer extends StatelessWidget { // final List snoreValues; // final List barData; // final List showLabel; // final int startTime; // final int endTime; // const SnoreChartContainer({ // required this.snoreValues, // required this.barData, // required this.showLabel, // required this.startTime, // required this.endTime, // super.key, // }); // @override // Widget build(BuildContext context) { // return Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // SnoreBarOverlay( // barData: barData, // showLabel: showLabel, // startTime: startTime, // endTime: endTime, // ), // Container(height: 32.rpx), // Container( // height: 23.rpx, // child: SnoreWaveform( // snoreValues: snoreValues, // startTime: startTime, // endTime: endTime, // ), // ), // ], // ); // } // } // class SnoreBarOverlay extends StatelessWidget { // final List barData; // final List showLabel; // final int startTime; // final int endTime; // const SnoreBarOverlay({ // required this.barData, // required this.showLabel, // required this.startTime, // required this.endTime, // super.key, // }); // @override // Widget build(BuildContext context) { // const double barHeight = 50; // return SizedBox( // height: barHeight, // child: CustomPaint( // size: Size(double.infinity, barHeight), // painter: SnoreBarPainter( // barData: barData, // showLabel: showLabel, // startTime: startTime, // endTime: endTime, // ), // ), // ); // } // } // class SnoreBarPainter extends CustomPainter { // final List barData; // final List showLabel; // final int startTime; // final int endTime; // SnoreBarPainter({ // required this.barData, // required this.showLabel, // required this.startTime, // required this.endTime, // }); // @override // void paint(Canvas canvas, Size size) { // final double width = size.width; // final double height = size.height; // final double pixelPerMs = width / (endTime - startTime); // for (var item in barData) { // final int st = item['st']; // final int et = item['et']; // final int type = item['type']; // int heightInit = 1; // final match = showLabel.firstWhere( // (e) => e['type'] == type, // orElse: () => null, // ); // Color barColor = Colors.transparent; // if (match != null) { // final dynamic colorStr = match['color']; // if (colorStr != null && colorStr.toString().isNotEmpty) { // barColor = stringToColor(colorStr); // } // } // final Paint barPaint = Paint() // ..color = barColor // ..style = PaintingStyle.fill; // final double leftX = (st - startTime) * pixelPerMs; // final double rightX = (et - startTime) * pixelPerMs; // final double barWidth = rightX - leftX; // //rem 深睡 中 // //浅睡 低 // //其他 高 // if (type == 1) { // heightInit = 1; // } else if (type == 2 || type == 6) { // heightInit = 2; // } else { // heightInit = 3; // } // final double barHeight = (heightInit + 5).toDouble() * 8; // final double top = height - barHeight; // final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight); // canvas.drawRect(rect, barPaint); // } // } // @override // bool shouldRepaint(covariant CustomPainter oldDelegate) => true; // } // class SnoreWaveform extends StatelessWidget { // final List snoreValues; // final int startTime; // final int endTime; // const SnoreWaveform({ // required this.snoreValues, // required this.startTime, // required this.endTime, // super.key, // }); // @override // Widget build(BuildContext context) { // return SizedBox( // height: 150, // child: LayoutBuilder( // builder: (context, constraints) { // return CustomPaint( // size: Size(constraints.maxWidth, constraints.maxHeight), // painter: SnoreWaveformPainter( // snoreValues: snoreValues, // startTime: startTime, // endTime: endTime, // ), // ); // }, // ), // ); // } // } // class SnoreWaveformPainter extends CustomPainter { // final List snoreValues; // final int startTime; // final int endTime; // SnoreWaveformPainter({ // required this.snoreValues, // required this.startTime, // required this.endTime, // }); // @override // void paint(Canvas canvas, Size size) { // final double width = size.width; // final double height = size.height; // if (width <= 0 || height <= 0) return; // final double totalDuration = (endTime - startTime).toDouble(); // if (totalDuration <= 0) return; // final double pixelPerMs = width / totalDuration; // // 过滤在时间范围内的有效事件 // final validEvents = snoreValues.where((e) { // final int st = e['st']; // final int et = e['et']; // // 事件与时间范围有重叠 // return !(et <= startTime || st >= endTime); // }).toList(); // // 计算中心线位置 // final double centerY = height / 2; // // 统一使用一个颜色(打鼾颜色) // final Color snoreColor = stringToColor("#8E7DEF").withOpacity(0.8); // final Paint barPaint = Paint() // ..color = snoreColor // ..style = PaintingStyle.fill; // final Paint borderPaint = Paint() // ..color = snoreColor.withOpacity(0.9) // ..style = PaintingStyle.stroke // ..strokeWidth = 0.5; // // 固定高度(上下对称) // final double fixedBarHeight = height * 0.3; // 固定为画布高度的30% // // 绘制每个打鼾事件(上下对称的柱状图) // for (final event in validEvents) { // final int st = event['st']; // final int et = event['et']; // // 计算绘制位置(裁剪到可视范围内) // final double startX = (st - startTime) * pixelPerMs; // final double endX = (et - startTime) * pixelPerMs; // // 确保在画布范围内 // if (endX < 0 || startX > width) continue; // final double drawStartX = startX.clamp(0, width); // final double drawEndX = endX.clamp(0, width); // final double drawWidth = drawEndX - drawStartX; // if (drawWidth <= 0) continue; // // 绘制上方的柱状图 // final double topBarTop = centerY - fixedBarHeight; // final Rect topRect = // Rect.fromLTWH(drawStartX, topBarTop, drawWidth, fixedBarHeight); // canvas.drawRect(topRect, barPaint); // canvas.drawRect(topRect, borderPaint); // // 绘制下方的柱状图(对称) // final double bottomBarTop = centerY; // final Rect bottomRect = // Rect.fromLTWH(drawStartX, bottomBarTop, drawWidth, fixedBarHeight); // canvas.drawRect(bottomRect, barPaint); // canvas.drawRect(bottomRect, borderPaint); // } // // 绘制中心线 // final Paint axisPaint = Paint() // ..color = Colors.grey.withOpacity(0.3) // ..strokeWidth = 0.5; // canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint); // // 绘制时间轴标签 // final textPainter = TextPainter( // textAlign: TextAlign.center, // textDirection: ui.TextDirection.ltr, // ); // final int hourMs = 60 * 60 * 1000; // final int totalHours = (endTime - startTime) ~/ hourMs; // // 创建开始和结束时间的DateTime对象 // final DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); // final DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); // // 1. 始终显示开始时间 // String label = DateFormat('HH:mm').format(startDt); // textPainter.text = TextSpan( // text: label, // style: TextStyle(fontSize: 10, color: Colors.grey), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(0 - textPainter.width / 2, height + 2), // ); // // 2. 决定显示策略 // if (totalHours <= 8) { // // 小时间段:显示所有整点小时(基于实际时间) // DateTime currentHour = DateTime( // startDt.year, // startDt.month, // startDt.day, // startDt.hour + 1, // 从下一个整点开始 // 0, // 0, // 0, // 0); // // 如果开始时间本身就是整点,需要调整 // if (startDt.minute == 0 && // startDt.second == 0 && // startDt.millisecond == 0) { // currentHour = startDt; // } // while (currentHour.millisecondsSinceEpoch < endTime) { // int timeMs = currentHour.millisecondsSinceEpoch; // // 确保时间在范围内 // if (timeMs > startTime && timeMs < endTime) { // double x = (timeMs - startTime) * pixelPerMs; // // 跳过太接近边界的时间点(30分钟内不显示) // if (timeMs - startTime < 10 * 60 * 1000 || // endTime - timeMs < 10 * 60 * 1000) { // currentHour = currentHour.add(Duration(hours: 1)); // continue; // } // label = "${currentHour.hour}"; // textPainter.text = TextSpan( // text: label, // style: TextStyle(fontSize: 10, color: Colors.grey), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(x - textPainter.width / 2, height + 2), // ); // } // currentHour = currentHour.add(Duration(hours: 1)); // } // } else { // // 长时间段:使用自适应间隔 // int labelInterval = (totalHours / 6).ceil(); // // 计算第一个整点标签(对齐整点小时) // DateTime firstLabelHour = DateTime( // startDt.year, // startDt.month, // startDt.day, // startDt.hour + (labelInterval - (startDt.hour % labelInterval)), // 0, // 0, // 0, // 0); // // 如果第一个标签在开始时间之前,调整到下一个间隔 // if (firstLabelHour.millisecondsSinceEpoch <= startTime) { // firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); // } // // 绘制中间标签 // DateTime currentHour = firstLabelHour; // while (currentHour.millisecondsSinceEpoch < endTime) { // int timeMs = currentHour.millisecondsSinceEpoch; // // 跳过太接近边界的时间点(1小时内不显示) // if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { // double x = (timeMs - startTime) * pixelPerMs; // label = "${currentHour.hour}"; // textPainter.text = TextSpan( // text: label, // style: TextStyle(fontSize: 10, color: Colors.grey), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(x - textPainter.width / 2, height + 2), // ); // } // currentHour = currentHour.add(Duration(hours: labelInterval)); // } // } // // 3. 始终显示结束时间 // label = DateFormat('HH:mm').format(endDt); // textPainter.text = TextSpan( // text: label, // style: TextStyle(fontSize: 10, color: Colors.grey), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(width - textPainter.width / 2, height + 2), // ); // } // @override // bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) { // return oldDelegate.snoreValues != snoreValues || // oldDelegate.startTime != startTime || // oldDelegate.endTime != endTime; // } // } import 'dart:math' as math; import 'dart:ui' as ui; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; import 'package:intl/intl.dart'; class SnoreChartContainer extends StatefulWidget { final List snoreValues; final List barData; final List showLabel; final int startTime; final int endTime; const SnoreChartContainer({ required this.snoreValues, required this.barData, required this.showLabel, required this.startTime, required this.endTime, super.key, }); @override _SnoreChartContainerState createState() => _SnoreChartContainerState(); } class _SnoreChartContainerState extends State { Offset? _hoverPosition; dynamic _selectedData; bool _isSelected = false; int? _selectedStartTime; Rect? _selectedRect; void _clearSelection() { setState(() { _hoverPosition = null; _selectedData = null; _isSelected = false; _selectedStartTime = null; _selectedRect = null; }); } void _handleClick(Offset localPosition, Size size) { final double pixelPerMs = size.width / (widget.endTime - widget.startTime); final int clickTime = widget.startTime + (localPosition.dx / pixelPerMs).round(); // 如果点击的是已经选中的区域,则取消选择 if (_isSelected && _selectedStartTime == clickTime) { _clearSelection(); return; } // 查找匹配的数据 dynamic foundData; Rect? foundRect; // 首先在barData中查找(睡眠阶段) for (var item in widget.barData) { final int st = item['st']; final int et = item['et']; if (clickTime >= st && clickTime <= et) { foundData = { 'type': 'sleep_stage', 'data': item, 'label': _getSleepStageLabel(item['type']), 'color': _getSleepStageColor(item['type']), }; // 计算选中区域的位置 final double leftX = (st - widget.startTime) * pixelPerMs; final double rightX = (et - widget.startTime) * pixelPerMs; foundRect = Rect.fromLTWH( leftX, 0, rightX - leftX, size.height, ); break; } } // 如果在barData中没找到,再在snoreValues中查找 if (foundData == null) { for (var item in widget.snoreValues) { final int st = item['st']; final int et = item['et']; if (clickTime >= st && clickTime <= et) { foundData = { 'type': 'snore_event', 'data': item, 'label': '打鼾事件', 'color': stringToColor("#8E7DEF"), }; // 计算选中区域的位置 final double leftX = (st - widget.startTime) * pixelPerMs; final double rightX = (et - widget.startTime) * pixelPerMs; foundRect = Rect.fromLTWH( leftX, 0, rightX - leftX, size.height, ); break; } } } if (foundData != null) { setState(() { _hoverPosition = localPosition; _selectedData = foundData; _isSelected = true; _selectedStartTime = clickTime; _selectedRect = foundRect; }); } else { // 点击空白区域,清除选择 _clearSelection(); } } void _handleHover(Offset localPosition, Size size) { if (_isSelected) return; // 如果已经有选中项,不处理悬停 final double pixelPerMs = size.width / (widget.endTime - widget.startTime); final int hoverTime = widget.startTime + (localPosition.dx / pixelPerMs).round(); // 查找匹配的数据 dynamic foundData; // 首先在barData中查找(睡眠阶段) for (var item in widget.barData) { final int st = item['st']; final int et = item['et']; if (hoverTime >= st && hoverTime <= et) { foundData = { 'type': 'sleep_stage', 'data': item, 'label': _getSleepStageLabel(item['type']), 'color': _getSleepStageColor(item['type']), }; break; } } // 如果在barData中没找到,再在snoreValues中查找 if (foundData == null) { for (var item in widget.snoreValues) { final int st = item['st']; final int et = item['et']; if (hoverTime >= st && hoverTime <= et) { foundData = { 'type': 'snore_event', 'data': item, 'label': '打鼾事件', 'color': stringToColor("#8E7DEF"), }; break; } } } if (foundData != null) { setState(() { _hoverPosition = localPosition; _selectedData = foundData; }); } else { setState(() { _hoverPosition = null; _selectedData = null; }); } } void _handleHoverExit() { if (!_isSelected) { setState(() { _hoverPosition = null; _selectedData = null; }); } } String _getSleepStageLabel(int type) { for (var label in widget.showLabel) { if (label['type'] == type) { return label['name']?.toString() ?? '未知'; } } return '未知'; } Color _getSleepStageColor(int type) { for (var label in widget.showLabel) { if (label['type'] == type) { final dynamic colorStr = label['color']; if (colorStr != null && colorStr.toString().isNotEmpty) { return stringToColor(colorStr.toString()); } } } return Colors.grey; } @override Widget build(BuildContext context) { return Container( child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (details) { final size = context.size ?? Size.zero; if (size.width > 0 && size.height > 0) { final localPosition = details.localPosition; _handleClick(localPosition, size); } }, child: MouseRegion( onHover: (details) { if (!_isSelected) { final size = context.size ?? Size.zero; if (size.width > 0 && size.height > 0) { final localPosition = details.localPosition; _handleHover(localPosition, size); } } }, onExit: (_) { _handleHoverExit(); }, child: Stack( clipBehavior: Clip.none, children: [ // 主要图表内容 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SnoreBarOverlay( barData: widget.barData, showLabel: widget.showLabel, startTime: widget.startTime, endTime: widget.endTime, selectedStartTime: _isSelected ? _selectedStartTime : null, ), Container(height: 32.rpx), Container( height: 23.rpx, child: SnoreWaveform( snoreValues: widget.snoreValues, startTime: widget.startTime, endTime: widget.endTime, selectedStartTime: _isSelected ? _selectedStartTime : null, ), ), ], ), // 高亮层 if (_selectedData != null && _selectedRect != null) Positioned.fill( child: CustomPaint( painter: HoverHighlightPainter( selectedRect: _selectedRect!, isSelected: _isSelected, ), ), ), // 提示框 if (_selectedData != null && _hoverPosition != null) _buildTooltipWidget(_selectedData!, _hoverPosition!), ], ), ), ), ); } Widget _buildTooltipWidget(dynamic data, Offset hoverPosition) { return Positioned( left: hoverPosition.dx < 50 ? hoverPosition.dx : hoverPosition.dx - 100, top: hoverPosition.dy - 100, child: IgnorePointer( child: _buildTooltipContent(data), ), ); } Widget _buildTooltipContent(dynamic data) { final int st = data['data']['st']; final int et = data['data']['et']; final DateTime startTime = DateTime.fromMillisecondsSinceEpoch(st); final DateTime endTime = DateTime.fromMillisecondsSinceEpoch(et); final String duration = _formatDuration(et - st); return Container( // width: 200.rpx, padding: EdgeInsets.all(16.rpx), // ✅ 容器内部边距,给文字留空间 decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(20.rpx), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 8, offset: Offset(0, 4), ), ], // border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: data['color'], shape: BoxShape.circle, ), ), SizedBox(width: 8), Text( data['label'], style: TextStyle( color: data['color'], fontSize: 20.rpx, ), ), ], ), SizedBox(height: 8), Text( "开始".tr + ':${DateFormat('HH:mm:ss').format(startTime)}', style: TextStyle( color: Color(0XFFFFFFFF), // 多了一个 F,建议改成正确格式 fontSize: 20.rpx, ), ), Text( "结束".tr + ':${DateFormat('HH:mm:ss').format(endTime)}', style: TextStyle( color: Color(0XFFFFFFFF), // 多了一个 F,建议改成正确格式 fontSize: 20.rpx, ), ), Text( "时长".tr + '$duration', style: TextStyle( color: Color(0XFFFFFFFF), // 多了一个 F,建议改成正确格式 fontSize: 20.rpx, ), ), if (data['type'] == 'snore_event') Text( '打鼾强度: ${data['data']['value']?.toString() ?? '--'}', style: TextStyle( color: Color(0XFFFFFFFF), // 多了一个 F,建议改成正确格式 fontSize: 20.rpx, ), ), ], ), ); } String _formatDuration(int milliseconds) { final seconds = milliseconds ~/ 1000; final minutes = seconds ~/ 60; final remainingSeconds = seconds % 60; return '${minutes}分${remainingSeconds}秒'; } } class SnoreBarOverlay extends StatelessWidget { final List barData; final List showLabel; final int startTime; final int endTime; final int? selectedStartTime; const SnoreBarOverlay({ required this.barData, required this.showLabel, required this.startTime, required this.endTime, this.selectedStartTime, super.key, }); @override Widget build(BuildContext context) { const double barHeight = 50; return SizedBox( height: barHeight, child: CustomPaint( size: Size(double.infinity, barHeight), painter: SnoreBarPainter( barData: barData, showLabel: showLabel, startTime: startTime, endTime: endTime, selectedStartTime: selectedStartTime, ), ), ); } } class SnoreBarPainter extends CustomPainter { final List barData; final List showLabel; final int startTime; final int endTime; final int? selectedStartTime; SnoreBarPainter({ required this.barData, required this.showLabel, required this.startTime, required this.endTime, this.selectedStartTime, }); Color _getSleepStageColor(int type) { for (var label in showLabel) { if (label['type'] == type) { final dynamic colorStr = label['color']; if (colorStr != null && colorStr.toString().isNotEmpty) { return stringToColor(colorStr.toString()); } } } return Colors.transparent; } @override void paint(Canvas canvas, Size size) { final double width = size.width; final double height = size.height; final double pixelPerMs = width / (endTime - startTime); for (var item in barData) { final int st = item['st']; final int et = item['et']; final int type = item['type']; int heightInit = 1; Color barColor = _getSleepStageColor(type); bool isSelected = selectedStartTime != null && selectedStartTime! >= st && selectedStartTime! <= et; if (isSelected) { barColor = barColor.withOpacity(0.9); } final Paint barPaint = Paint() ..color = barColor ..style = PaintingStyle.fill; final double leftX = (st - startTime) * pixelPerMs; final double rightX = (et - startTime) * pixelPerMs; final double barWidth = math.max(rightX - leftX, 1.0); if (type == 1) { heightInit = 1; } else if (type == 2 || type == 6) { heightInit = 2; } else { heightInit = 3; } final double barHeight = (heightInit + 5).toDouble() * 8; final double top = height - barHeight; final rect = Rect.fromLTWH(leftX, top, barWidth, barHeight); canvas.drawRect(rect, barPaint); if (isSelected) { final Paint borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..strokeJoin = StrokeJoin.round; canvas.drawRect(rect, borderPaint); final Paint innerGlowPaint = Paint() ..color = Colors.white.withOpacity(0.3) ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawRect(rect, innerGlowPaint); } } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return oldDelegate is! SnoreBarPainter || oldDelegate.barData != barData || oldDelegate.selectedStartTime != selectedStartTime; } } class SnoreWaveform extends StatelessWidget { final List snoreValues; final int startTime; final int endTime; final int? selectedStartTime; const SnoreWaveform({ required this.snoreValues, required this.startTime, required this.endTime, this.selectedStartTime, super.key, }); @override Widget build(BuildContext context) { return SizedBox( height: 150, child: LayoutBuilder( builder: (context, constraints) { return CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: SnoreWaveformPainter( snoreValues: snoreValues, startTime: startTime, endTime: endTime, selectedStartTime: selectedStartTime, ), ); }, ), ); } } class SnoreWaveformPainter extends CustomPainter { final List snoreValues; final int startTime; final int endTime; final int? selectedStartTime; SnoreWaveformPainter({ required this.snoreValues, required this.startTime, required this.endTime, this.selectedStartTime, }); @override void paint(Canvas canvas, Size size) { final double width = size.width; final double height = size.height; if (width <= 0 || height <= 0) return; final double totalDuration = (endTime - startTime).toDouble(); if (totalDuration <= 0) return; final double pixelPerMs = width / totalDuration; final validEvents = snoreValues.where((e) { final int st = e['st']; final int et = e['et']; return !(et <= startTime || st >= endTime); }).toList(); final double centerY = height / 2; for (final event in validEvents) { final int st = event['st']; final int et = event['et']; bool isSelected = selectedStartTime != null && selectedStartTime! >= st && selectedStartTime! <= et; Color snoreColor = stringToColor("#8E7DEF").withOpacity(0.8); if (isSelected) { snoreColor = stringToColor("#8E7DEF").withOpacity(0.95); } final Paint barPaint = Paint() ..color = snoreColor ..style = PaintingStyle.fill; final Paint borderPaint = Paint() ..color = snoreColor.withOpacity(0.9) ..style = PaintingStyle.stroke ..strokeWidth = isSelected ? 2.0 : 0.5; final double fixedBarHeight = height * 0.3; final double startX = (st - startTime) * pixelPerMs; final double endX = (et - startTime) * pixelPerMs; if (endX < 0 || startX > width) continue; final double drawStartX = math.max(startX, 0); final double drawEndX = math.min(endX, width); final double drawWidth = math.max(drawEndX - drawStartX, 1.0); if (drawWidth <= 0) continue; final double topBarTop = centerY - fixedBarHeight; final Rect topRect = Rect.fromLTWH(drawStartX, topBarTop, drawWidth, fixedBarHeight); canvas.drawRect(topRect, barPaint); canvas.drawRect(topRect, borderPaint); final double bottomBarTop = centerY; final Rect bottomRect = Rect.fromLTWH(drawStartX, bottomBarTop, drawWidth, fixedBarHeight); canvas.drawRect(bottomRect, barPaint); canvas.drawRect(bottomRect, borderPaint); if (isSelected) { final Paint highlightPaint = Paint() ..color = Colors.white.withOpacity(0.3) ..style = PaintingStyle.fill; final Rect highlightRect = Rect.fromLTWH( drawStartX - 2, centerY - fixedBarHeight - 2, drawWidth + 4, fixedBarHeight * 2 + 4); canvas.drawRect(highlightRect, highlightPaint); final Paint glowPaint = Paint() ..color = stringToColor("#8E7DEF").withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 3.0 ..maskFilter = MaskFilter.blur(BlurStyle.normal, 5.0); final Rect glowRect = Rect.fromLTWH( drawStartX - 3, centerY - fixedBarHeight - 3, drawWidth + 6, fixedBarHeight * 2 + 6); canvas.drawRect(glowRect, glowPaint); } } final Paint axisPaint = Paint() ..color = Colors.grey.withOpacity(0.3) ..strokeWidth = 0.5; canvas.drawLine(Offset(0, centerY), Offset(width, centerY), axisPaint); final textPainter = TextPainter( textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); final int hourMs = 60 * 60 * 1000; final int totalHours = (endTime - startTime) ~/ hourMs; final DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); final DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); String label = DateFormat('HH:mm').format(startDt); textPainter.text = TextSpan( text: label, style: TextStyle(fontSize: 10, color: Colors.grey), ); textPainter.layout(); textPainter.paint( canvas, Offset(0 - textPainter.width / 2, height + 2), ); if (totalHours <= 8) { DateTime currentHour = DateTime(startDt.year, startDt.month, startDt.day, startDt.hour + 1, 0, 0, 0, 0); if (startDt.minute == 0 && startDt.second == 0 && startDt.millisecond == 0) { currentHour = startDt; } while (currentHour.millisecondsSinceEpoch < endTime) { int timeMs = currentHour.millisecondsSinceEpoch; if (timeMs > startTime && timeMs < endTime) { double x = (timeMs - startTime) * pixelPerMs; if (timeMs - startTime < 10 * 60 * 1000 || endTime - timeMs < 10 * 60 * 1000) { currentHour = currentHour.add(Duration(hours: 1)); continue; } label = "${currentHour.hour}"; textPainter.text = TextSpan( text: label, style: TextStyle(fontSize: 10, color: Colors.grey), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, height + 2), ); } currentHour = currentHour.add(Duration(hours: 1)); } } else { int labelInterval = (totalHours / 6).ceil(); DateTime firstLabelHour = DateTime( startDt.year, startDt.month, startDt.day, startDt.hour + (labelInterval - (startDt.hour % labelInterval)), 0, 0, 0, 0); if (firstLabelHour.millisecondsSinceEpoch <= startTime) { firstLabelHour = firstLabelHour.add(Duration(hours: labelInterval)); } DateTime currentHour = firstLabelHour; while (currentHour.millisecondsSinceEpoch < endTime) { int timeMs = currentHour.millisecondsSinceEpoch; if (timeMs - startTime >= hourMs && endTime - timeMs >= hourMs) { double x = (timeMs - startTime) * pixelPerMs; label = "${currentHour.hour}"; textPainter.text = TextSpan( text: label, style: TextStyle(fontSize: 10, color: Colors.grey), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, height + 2), ); } currentHour = currentHour.add(Duration(hours: labelInterval)); } } label = DateFormat('HH:mm').format(endDt); textPainter.text = TextSpan( text: label, style: TextStyle(fontSize: 10, color: Colors.grey), ); textPainter.layout(); textPainter.paint( canvas, Offset(width - textPainter.width / 2, height + 2), ); } @override bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) { return oldDelegate.snoreValues != snoreValues || oldDelegate.startTime != startTime || oldDelegate.endTime != endTime || oldDelegate.selectedStartTime != selectedStartTime; } } class HoverHighlightPainter extends CustomPainter { final Rect selectedRect; final bool isSelected; HoverHighlightPainter({ required this.selectedRect, required this.isSelected, }); @override void paint(Canvas canvas, Size size) { if (!isSelected) return; final Paint highlightPaint = Paint() ..color = Colors.black.withOpacity(0.1) ..style = PaintingStyle.fill; final Rect highlightRect = Rect.fromLTWH( selectedRect.left.clamp(0, size.width), 0, selectedRect.width.clamp(0, size.width - selectedRect.left), size.height, ); canvas.drawRect(highlightRect, highlightPaint); final Paint borderPaint = Paint() ..color = Colors.blue.withOpacity(0.3) ..style = PaintingStyle.stroke ..strokeWidth = 1.0; canvas.drawRect(highlightRect, borderPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }