Files
tuiche/lib/pages/sleep_report/chart/SnoreWaveform.dart
2025-12-10 15:22:17 +08:00

340 lines
9.0 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: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<dynamic> snoreValues;
final List<dynamic> barData;
final List<dynamic> 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<dynamic> barData;
final List<dynamic> 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<dynamic> barData;
final List<dynamic> 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<dynamic> 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<dynamic> 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<double>(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;
}