// 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) { // // barData.add({ // // "type": 6, // // "et": 1765291778963, // // "st": 1765289341000, // // }); // 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: CustomPaint( // size: Size(double.infinity, 150), // 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; // final double centerY = height / 2; // final double totalDuration = (endTime - startTime).toDouble(); // final double pixelPerMs = width / totalDuration; // final Paint wavePaint = Paint() // ..color = stringToColor("#8E7DEF").withOpacity(0.8) // ..strokeWidth = 1.5 // ..style = PaintingStyle.stroke; // final Path upperPath = Path(); // final Path lowerPath = Path(); // double maxValue = snoreValues.fold(0, (prev, e) { // final value = e["value"]?.toDouble() ?? 0; // return value > prev ? value : prev; // }); // final double maxWaveHeight = height * 1; // final double scaleY = maxValue > 0 ? (maxWaveHeight / maxValue) : 1; // for (int i = 0; i < snoreValues.length; i++) { // final timestamp = snoreValues[i]["st"]; // final value = snoreValues[i]["value"]?.toDouble() ?? 0; // final x = (timestamp - startTime) * pixelPerMs; // final y = centerY - value * scaleY; // final yMirror = centerY + value * scaleY; // if (i == 0) { // upperPath.moveTo(x, y); // lowerPath.moveTo(x, yMirror); // } else { // upperPath.lineTo(x, y); // lowerPath.lineTo(x, yMirror); // } // } // canvas.drawPath(upperPath, wavePaint); // canvas.drawPath(lowerPath, wavePaint); // final Paint axisPaint = Paint() // ..color = Colors.grey.withOpacity(0.6) // ..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; // // 1. 始终显示开始时间 // double x = 0; // DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); // 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(x - textPainter.width / 2, height + 2), // ); // // 2. 决定显示策略 // if (totalHours <= 8) { // // 小时间段:显示所有整点小时 // for (int t = startTime + hourMs; t < endTime; t += hourMs) { // x = (t - startTime) * pixelPerMs; // DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); // // 判断是否接近边界(30分钟内不显示) // if (t - startTime < 30 * 60 * 1000 || endTime - t < 30 * 60 * 1000) { // continue; // } // label = dt.hour == 0 ? "0" : "${dt.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), // ); // } // } else { // // 长时间段:使用自适应间隔 // int labelInterval = (totalHours / 6).ceil(); // // 计算第一个标签位置(对齐整点) // int firstLabelMs = // ((startTime ~/ (labelInterval * hourMs))) * labelInterval * hourMs; // if (firstLabelMs <= startTime) { // firstLabelMs += labelInterval * hourMs; // } // // 绘制中间标签 // for (int t = firstLabelMs; t < endTime; t += labelInterval * hourMs) { // // 跳过太接近边界的时间点(1小时内不显示) // if (t - startTime < hourMs || endTime - t < hourMs) continue; // x = (t - startTime) * pixelPerMs; // DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); // label = dt.hour == 0 ? "0" : "${dt.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), // ); // } // } // // 3. 始终显示结束时间 // x = (endTime - startTime) * pixelPerMs; // DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); // label = DateFormat('HH:mm').format(endDt); // textPainter.text = TextSpan( // text: label, // style: TextStyle(fontSize: 10, color: Colors.grey), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(x - textPainter.width / 2, height + 2), // ); // } // @override // bool shouldRepaint(covariant CustomPainter oldDelegate) => true; // } 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(); if (validEvents.isEmpty) { // 绘制无数据提示 final textPainter = TextPainter( text: TextSpan( text: '无打鼾数据', style: TextStyle(color: Colors.grey, fontSize: 12), ), textDirection: ui.TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(width / 2 - textPainter.width / 2, height / 2 - 10)); return; } // 计算中心线位置 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; // 1. 始终显示开始时间 double x = 0; DateTime startDt = DateTime.fromMillisecondsSinceEpoch(startTime); 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(x - textPainter.width / 2, height + 2), ); // 2. 决定显示策略 if (totalHours <= 8) { // 小时间段:显示所有整点小时 for (int t = startTime + hourMs; t < endTime; t += hourMs) { x = (t - startTime) * pixelPerMs; DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); // 判断是否接近边界(30分钟内不显示) if (t - startTime < 30 * 60 * 1000 || endTime - t < 30 * 60 * 1000) { continue; } label = dt.hour == 0 ? "0" : "${dt.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), ); } } else { // 长时间段:使用自适应间隔 int labelInterval = (totalHours / 6).ceil(); // 计算第一个标签位置(对齐整点) int firstLabelMs = ((startTime ~/ (labelInterval * hourMs))) * labelInterval * hourMs; if (firstLabelMs <= startTime) { firstLabelMs += labelInterval * hourMs; } // 绘制中间标签 for (int t = firstLabelMs; t < endTime; t += labelInterval * hourMs) { // 跳过太接近边界的时间点(1小时内不显示) if (t - startTime < hourMs || endTime - t < hourMs) continue; x = (t - startTime) * pixelPerMs; DateTime dt = DateTime.fromMillisecondsSinceEpoch(t); label = dt.hour == 0 ? "0" : "${dt.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), ); } } // 3. 始终显示结束时间 x = (endTime - startTime) * pixelPerMs; DateTime endDt = DateTime.fromMillisecondsSinceEpoch(endTime); label = DateFormat('HH:mm').format(endDt); textPainter.text = TextSpan( text: label, style: TextStyle(fontSize: 10, color: Colors.grey), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, height + 2), ); } @override bool shouldRepaint(covariant SnoreWaveformPainter oldDelegate) { return oldDelegate.snoreValues != snoreValues || oldDelegate.startTime != startTime || oldDelegate.endTime != endTime; } }