更新睡眠报告

This commit is contained in:
wyf
2025-05-29 20:20:49 +08:00
parent b34737dbe8
commit 7a816922fa
41 changed files with 4604 additions and 2394 deletions

View File

@@ -50,9 +50,7 @@ class FatigueCircleIndicator extends StatelessWidget {
'$percent%',
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: percent > 60
? themeController.currentColor.sc9
: themeController.currentColor.sc3,
color: color,
),
),
SizedBox(height: 4.rpx),
@@ -60,9 +58,7 @@ class FatigueCircleIndicator extends StatelessWidget {
explain,
style: TextStyle(
fontSize: AppConstants().normal_text_fontSize,
color: percent > 60
? themeController.currentColor.sc9
: themeController.currentColor.sc3,
color: color,
),
),
],

View File

@@ -5,69 +5,517 @@ import 'package:vbvs_app/common/util/MyUtils.dart';
import 'dart:ui' as ui;
import 'dart:math';
class LineChartByRange extends StatelessWidget {
//根据数据自定义
// class LineChartByRange extends StatefulWidget {
// final List<Map<String, dynamic>> showLabel;
// final int startTime;
// final int endTime;
// final int? threshold;
// const LineChartByRange({
// Key? key,
// required this.showLabel,
// required this.startTime,
// required this.endTime,
// this.threshold, // 新增
// }) : super(key: key);
// @override
// State<LineChartByRange> createState() => _LineChartByRangeState();
// }
// class _LineChartByRangeState extends State<LineChartByRange> {
// Offset? selectedOffset;
// Map<String, dynamic>? selectedData;
// @override
// Widget build(BuildContext context) {
// if (widget.showLabel.isEmpty) return const SizedBox();
// int maxTimes = widget.showLabel
// .map((e) => e['times'] ?? 0)
// .reduce((a, b) => a > b ? a : b);
// int yMax = (maxTimes / 10).ceil() * 10;
// if (yMax == 0) yMax = 10;
// DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime);
// DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime);
// return GestureDetector(
// onTapDown: (details) {
// RenderBox box = context.findRenderObject() as RenderBox;
// final localPosition = box.globalToLocal(details.globalPosition);
// // 查找是否点击到某个点
// for (var item in widget.showLabel) {
// int start = item['startTime'];
// int end = item['endTime'];
// int times = item['times'];
// double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理
// double chartHeight = box.size.height - 30.rpx;
// double xStart = 20.rpx + 12.rpx;
// int totalDuration =
// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
// double startX = xStart +
// chartWidth *
// (start - minTime.millisecondsSinceEpoch) /
// totalDuration;
// double y = chartHeight * (1 - times / yMax);
// // 判断点击范围圆点半径±6.rpx范围
// if ((localPosition - Offset(startX, y)).distance < 10.rpx) {
// setState(() {
// selectedOffset = Offset(startX, y);
// selectedData = item;
// });
// return;
// }
// double endX = xStart +
// chartWidth *
// (end - minTime.millisecondsSinceEpoch) /
// totalDuration;
// if ((localPosition - Offset(endX, y)).distance < 10.rpx) {
// setState(() {
// selectedOffset = Offset(endX, y);
// selectedData = item;
// });
// return;
// }
// }
// // 没点到,清除选中
// setState(() {
// selectedOffset = null;
// selectedData = null;
// });
// },
// child: Stack(
// children: [
// SizedBox(
// height: 500.rpx,
// child: CustomPaint(
// size: Size(double.infinity, 500.rpx),
// painter: _LineChartByRangePainter(
// data: widget.showLabel,
// yMax: yMax,
// minTime: minTime,
// maxTime: maxTime,
// threshold: widget.threshold, // 新增
// ),
// ),
// ),
// if (selectedOffset != null && selectedData != null)
// Positioned(
// left: selectedOffset!.dx - 60.rpx,
// top: selectedOffset!.dy - 50.rpx,
// child: Container(
// padding:
// EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx),
// decoration: BoxDecoration(
// color: Colors.black.withOpacity(0.3),
// borderRadius: BorderRadius.circular(10.rpx),
// ),
// child: Text(
// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - '
// '${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n'
// '次数: ${selectedData!['times']}',
// style: TextStyle(
// fontSize: 18.rpx,
// color: Colors.white,
// ),
// ),
// ),
// ),
// ],
// ),
// );
// }
// }
// class _LineChartByRangePainter extends CustomPainter {
// final List<Map<String, dynamic>> data;
// final int yMax;
// final DateTime minTime;
// final DateTime maxTime;
// final int? threshold;
// _LineChartByRangePainter({
// required this.data,
// required this.yMax,
// required this.minTime,
// required this.maxTime,
// this.threshold,
// });
// @override
// void paint(Canvas canvas, Size size) {
// double padding = 20.rpx;
// double labelInset = 12.rpx;
// final double xStart = padding + labelInset;
// final double xEnd = size.width - padding - labelInset;
// final double chartWidth = xEnd - xStart;
// double chartHeight = size.height - 30.rpx;
// int totalDuration =
// maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
// if (totalDuration <= 0) return;
// Paint linePaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeWidth = 3.rpx
// ..color = stringToColor("#00C1AA")
// ..strokeCap = StrokeCap.round;
// Paint axisPaint = Paint()
// ..color = Colors.grey.withOpacity(0.4)
// ..strokeWidth = 1.rpx;
// Paint thresholdPaint = Paint()
// ..color = themeController.currentColor.sc9
// ..strokeWidth = 1.rpx;
// // 1. 阈值虚线(红色)
// if (threshold != null && threshold! >= 0 && threshold! <= yMax) {
// double yThreshold = chartHeight * (1 - threshold! / yMax);
// drawDashedLine(
// canvas,
// Offset(xStart, yThreshold),
// Offset(xEnd, yThreshold),
// thresholdPaint,
// dashWidth: 8.rpx,
// dashSpace: 6.rpx,
// );
// }
// // 2. 绘制数据线段和圆点
// for (var item in data) {
// int start = item['startTime'];
// int end = item['endTime'];
// int times = item['times'];
// double startX = xStart +
// chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
// double endX = xStart +
// chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
// double y = chartHeight * (1 - times / yMax);
// // 设置颜色(根据 threshold 判断)
// Color pointColor;
// if (threshold != null && times >= threshold!) {
// pointColor = themeController.currentColor.sc9;
// } else {
// pointColor = stringToColor("#00C1AA");
// }
// Paint dynamicLinePaint = Paint()
// ..style = PaintingStyle.stroke
// ..strokeWidth = 3.rpx
// ..color = pointColor
// ..strokeCap = StrokeCap.round;
// Paint dynamicCirclePaint = Paint()
// ..style = PaintingStyle.fill
// ..color = pointColor;
// // 画线段
// canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint);
// // 画起点和终点圆点
// canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint);
// canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint);
// }
// // 3. Y轴辅助线和文字
// for (int i = 0; i <= 6; i++) {
// double y = chartHeight * i / 6;
// if (i == 6) {
// canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
// } else {
// drawDashedLine(
// canvas,
// Offset(xStart, y),
// Offset(xEnd, y),
// axisPaint,
// dashWidth: 8.rpx,
// dashSpace: 6.rpx,
// );
// }
// TextPainter tp = TextPainter(
// text: TextSpan(
// text: '${yMax - (yMax * i / 6).round()}',
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// tp.layout();
// tp.paint(canvas, Offset(0, y - tp.height / 2));
// }
// // 4. X轴主线
// canvas.drawLine(
// Offset(xStart, chartHeight),
// Offset(xEnd, chartHeight),
// axisPaint,
// );
// // 5. X轴时间文字左右两侧
// String leftLabel = DateFormat('HH:mm').format(minTime);
// TextPainter leftTp = TextPainter(
// text: TextSpan(
// text: leftLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// leftTp.layout();
// leftTp.paint(canvas,
// Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx));
// String rightLabel = DateFormat('HH:mm').format(maxTime);
// TextPainter rightTp = TextPainter(
// text: TextSpan(
// text: rightLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// rightTp.layout();
// rightTp.paint(
// canvas,
// Offset(size.width - padding - labelInset - rightTp.width / 2,
// chartHeight + 8.rpx));
// // 6. 中间小时刻度
// int totalHours = maxTime.difference(minTime).inHours + 1;
// int startHour = minTime.hour;
// for (int i = 1; i < totalHours; i++) {
// double x = xStart + chartWidth * i / totalHours;
// int hourLabelNum = (startHour + i) % 24;
// String hourLabel = '$hourLabelNum';
// TextPainter tp = TextPainter(
// text: TextSpan(
// text: hourLabel,
// style: TextStyle(
// fontSize: 18.rpx,
// color: themeController.currentColor.sc4,
// ),
// ),
// textDirection: ui.TextDirection.ltr,
// );
// tp.layout();
// tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// void drawDashedLine(
// Canvas canvas,
// Offset start,
// Offset end,
// Paint paint, {
// required double dashWidth,
// required double dashSpace,
// }) {
// final dx = end.dx - start.dx;
// final dy = end.dy - start.dy;
// final distance = sqrt(dx * dx + dy * dy);
// final direction = Offset(dx / distance, dy / distance);
// double drawn = 0;
// while (drawn < distance) {
// final from = start + direction * drawn;
// final to = start + direction * (drawn + dashWidth).clamp(0, distance);
// canvas.drawLine(from, to, paint);
// drawn += dashWidth + dashSpace;
// }
// }
// }
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart';
import 'dart:ui' as ui;
import 'dart:math';
class LineChartByRange extends StatefulWidget {
final List<Map<String, dynamic>> showLabel;
final int startTime;
final int endTime;
final int? threshold;
/// 新增外部指定的 Y 轴最大值
final int maxY;
/// Y 轴分段数默认6段
final int ySegments;
const LineChartByRange({
Key? key,
required this.showLabel,
required this.startTime,
required this.endTime,
required this.maxY,
this.threshold,
this.ySegments = 6,
}) : super(key: key);
@override
State<LineChartByRange> createState() => _LineChartByRangeState();
}
class _LineChartByRangeState extends State<LineChartByRange> {
Offset? selectedOffset;
Map<String, dynamic>? selectedData;
@override
Widget build(BuildContext context) {
if (showLabel.isEmpty) return const SizedBox();
if (widget.showLabel.isEmpty) return const SizedBox();
int maxTimes =
showLabel.map((e) => e['times'] ?? 0).reduce((a, b) => a > b ? a : b);
int yMax = (maxTimes / 10).ceil() * 10;
if (yMax == 0) yMax = 10;
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(widget.startTime);
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(widget.endTime);
DateTime minTime = DateTime.fromMillisecondsSinceEpoch(startTime);
DateTime maxTime = DateTime.fromMillisecondsSinceEpoch(endTime);
return GestureDetector(
onTapDown: (details) {
RenderBox box = context.findRenderObject() as RenderBox;
final localPosition = box.globalToLocal(details.globalPosition);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 500.rpx,
child: CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: _LineChartByRangePainter(
data: showLabel,
yMax: yMax,
minTime: minTime,
maxTime: maxTime,
// 查找是否点击到某个点
for (var item in widget.showLabel) {
int start = item['startTime'];
int end = item['endTime'];
int times = item['times'];
double chartWidth = box.size.width - 40.rpx; // 与 painter 内一致处理
double chartHeight = box.size.height - 30.rpx;
double xStart = 20.rpx + 12.rpx;
int totalDuration =
maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
double startX = xStart +
chartWidth *
(start - minTime.millisecondsSinceEpoch) /
totalDuration;
double y = chartHeight * (1 - times / widget.maxY);
// 判断点击范围圆点半径±6.rpx范围
if ((localPosition - Offset(startX, y)).distance < 10.rpx) {
setState(() {
selectedOffset = Offset(startX, y);
selectedData = item;
});
return;
}
double endX = xStart +
chartWidth *
(end - minTime.millisecondsSinceEpoch) /
totalDuration;
if ((localPosition - Offset(endX, y)).distance < 10.rpx) {
setState(() {
selectedOffset = Offset(endX, y);
selectedData = item;
});
return;
}
}
// 没点到,清除选中
setState(() {
selectedOffset = null;
selectedData = null;
});
},
child: Stack(
children: [
SizedBox(
height: 500.rpx,
child: CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: _LineChartByRangePainter(
data: widget.showLabel,
maxY: widget.maxY,
minTime: minTime,
maxTime: maxTime,
threshold: widget.threshold,
ySegments: widget.ySegments,
),
),
),
),
],
if (selectedOffset != null && selectedData != null)
Positioned(
left: selectedOffset!.dx - 60.rpx,
top: selectedOffset!.dy - 50.rpx,
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 12.rpx, vertical: 8.rpx),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(10.rpx),
),
child: Text(
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['startTime']))} - '
'${DateFormat('HH:mm').format(DateTime.fromMillisecondsSinceEpoch(selectedData!['endTime']))}\n'
'次数: ${selectedData!['times']}',
style: TextStyle(
fontSize: 18.rpx,
color: Colors.white,
),
),
),
),
],
),
);
}
}
class _LineChartByRangePainter extends CustomPainter {
final List<Map<String, dynamic>> data;
final int yMax;
final int maxY;
final DateTime minTime;
final DateTime maxTime;
final int? threshold;
final int ySegments;
_LineChartByRangePainter({
required this.data,
required this.yMax,
required this.maxY,
required this.minTime,
required this.maxTime,
this.threshold,
this.ySegments = 6,
});
@override
void paint(Canvas canvas, Size size) {
double padding = 20.rpx;
double labelInset = 12.rpx; // X轴标签缩进距离
double labelInset = 12.rpx;
// 绘图X轴起止点考虑内缩labelInset
final double xStart = padding + labelInset;
final double xEnd = size.width - padding - labelInset;
final double chartWidth = xEnd - xStart;
@@ -78,17 +526,28 @@ class _LineChartByRangePainter extends CustomPainter {
maxTime.millisecondsSinceEpoch - minTime.millisecondsSinceEpoch;
if (totalDuration <= 0) return;
Paint linePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.rpx
..color = stringToColor("#00C1AA")
..strokeCap = StrokeCap.round;
Paint axisPaint = Paint()
..color = Colors.grey.withOpacity(0.4)
..strokeWidth = 1.rpx;
Paint fillCirclePaint = Paint()
..style = PaintingStyle.fill
..color = stringToColor("#00C1AA");
Paint thresholdPaint = Paint()
..color = themeController.currentColor.sc9
..strokeWidth = 1.rpx;
// 1. 先绘制数据线段及起止点圆点
// 阈值虚线(红色)
if (threshold != null && threshold! >= 0 && threshold! <= maxY) {
double yThreshold = chartHeight * (1 - threshold! / maxY);
drawDashedLine(
canvas,
Offset(xStart, yThreshold),
Offset(xEnd, yThreshold),
thresholdPaint,
dashWidth: 8.rpx,
dashSpace: 6.rpx,
);
}
// 绘制数据线段和圆点
for (var item in data) {
int start = item['startTime'];
int end = item['endTime'];
@@ -98,31 +557,41 @@ class _LineChartByRangePainter extends CustomPainter {
chartWidth * (start - minTime.millisecondsSinceEpoch) / totalDuration;
double endX = xStart +
chartWidth * (end - minTime.millisecondsSinceEpoch) / totalDuration;
double y = chartHeight * (1 - times / yMax);
double y = chartHeight * (1 - times / maxY);
// 设置颜色(根据 threshold 判断)
Color pointColor;
if (threshold != null && times >= threshold!) {
pointColor = themeController.currentColor.sc9;
} else {
pointColor = stringToColor("#00C1AA");
}
Paint dynamicLinePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3.rpx
..color = pointColor
..strokeCap = StrokeCap.round;
Paint dynamicCirclePaint = Paint()
..style = PaintingStyle.fill
..color = pointColor;
// 画线段
canvas.drawLine(Offset(startX, y), Offset(endX, y), linePaint);
canvas.drawLine(Offset(startX, y), Offset(endX, y), dynamicLinePaint);
// 画起点圆点
canvas.drawCircle(Offset(startX, y), 4.rpx, fillCirclePaint);
// 画终点圆点
canvas.drawCircle(Offset(endX, y), 4.rpx, fillCirclePaint);
// 画起点和终点圆点
canvas.drawCircle(Offset(startX, y), 6.rpx, dynamicCirclePaint);
canvas.drawCircle(Offset(endX, y), 6.rpx, dynamicCirclePaint);
}
// 2. Y轴辅助线文字
Paint axisPaint = Paint()
..color = Colors.grey.withOpacity(0.4)
..strokeWidth = 1.rpx;
// Y轴辅助线文字
for (int i = 0; i <= ySegments; i++) {
double y = chartHeight * i / ySegments;
for (int i = 0; i <= 6; i++) {
double y = chartHeight * i / 6;
if (i == 6) {
// 实线
if (i == ySegments) {
canvas.drawLine(Offset(xStart, y), Offset(xEnd, y), axisPaint);
} else {
// 虚线
drawDashedLine(
canvas,
Offset(xStart, y),
@@ -133,12 +602,13 @@ class _LineChartByRangePainter extends CustomPainter {
);
}
// Y轴文字
TextPainter tp = TextPainter(
text: TextSpan(
text: '${yMax - (yMax * i / 6).round()}',
text: '${maxY - (maxY * i / ySegments).round()}',
style: TextStyle(
fontSize: 18.rpx, color: themeController.currentColor.sc4),
fontSize: 18.rpx,
color: themeController.currentColor.sc4,
),
),
textDirection: ui.TextDirection.ltr,
);
@@ -146,29 +616,14 @@ class _LineChartByRangePainter extends CustomPainter {
tp.paint(canvas, Offset(0, y - tp.height / 2));
}
// 3. X轴线
// X轴线
canvas.drawLine(
Offset(xStart, chartHeight), Offset(xEnd, chartHeight), axisPaint);
Offset(xStart, chartHeight),
Offset(xEnd, chartHeight),
axisPaint,
);
// 4. 画X轴时间点对应的垂直虚线辅助线
int totalHours = maxTime.difference(minTime).inHours;
int startHour = minTime.hour;
// for (int i = 1; i < totalHours; i++) {
// double x = xStart + chartWidth * i / totalHours;
// // 垂直虚线
// drawDashedLine(
// canvas,
// Offset(x, 0),
// Offset(x, chartHeight),
// axisPaint,
// dashWidth: 4.rpx,
// dashSpace: 4.rpx,
// );
// }
// 5. 画左侧完整时分 (HH:mm),往内缩 labelInset
// X轴时间文字左右两侧
String leftLabel = DateFormat('HH:mm').format(minTime);
TextPainter leftTp = TextPainter(
text: TextSpan(
@@ -184,7 +639,6 @@ class _LineChartByRangePainter extends CustomPainter {
leftTp.paint(canvas,
Offset(padding + labelInset - leftTp.width / 2, chartHeight + 8.rpx));
// 6. 画右侧完整时分 (HH:mm),往内缩 labelInset
String rightLabel = DateFormat('HH:mm').format(maxTime);
TextPainter rightTp = TextPainter(
text: TextSpan(
@@ -202,7 +656,10 @@ class _LineChartByRangePainter extends CustomPainter {
Offset(size.width - padding - labelInset - rightTp.width / 2,
chartHeight + 8.rpx));
// 7. 中间小时数字(23, 0, 1, 2, ...)
// 中间小时刻度
int totalHours = maxTime.difference(minTime).inHours + 1;
int startHour = minTime.hour;
for (int i = 1; i < totalHours; i++) {
double x = xStart + chartWidth * i / totalHours;
@@ -220,7 +677,6 @@ class _LineChartByRangePainter extends CustomPainter {
textDirection: ui.TextDirection.ltr,
);
tp.layout();
tp.paint(canvas, Offset(x - tp.width / 2, chartHeight + 8.rpx));
}
}

View File

@@ -0,0 +1,199 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:intl/intl.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';
class BarData {
final int st; // 起始时间(毫秒)
final int et; // 结束时间(毫秒)
final double value; // 柱子高度
final int id;
final String name;
final Color color;
BarData({
required this.st,
required this.et,
required this.value,
required this.id,
required this.name,
required this.color,
});
}
class BarChartWidget extends StatelessWidget {
final List<BarData> data;
final int startTime; // 毫秒时间戳
final int endTime; // 毫秒时间戳
final double maxYValue; // Y轴最大值
final int yStepCount; // Y轴分段数
const BarChartWidget({
super.key,
required this.data,
required this.startTime,
required this.endTime,
required this.maxYValue,
this.yStepCount = 5,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(double.infinity, 500.rpx),
painter: BarChartPainter(
data,
startTime,
endTime,
maxYValue: maxYValue,
yStepCount: yStepCount,
),
);
}
}
class BarChartPainter extends CustomPainter {
final List<BarData> data;
final int startTime;
final int endTime;
final double maxYValue;
final int yStepCount;
final double topPadding = 0; // 控制顶部间距
final double bottomPadding = 0; // 控制底部间距
final double leftPadding = 30.rpx;
// final double labelHeight = 50.rpx;
BarChartPainter(
this.data,
this.startTime,
this.endTime, {
required this.maxYValue,
this.yStepCount = 5,
});
@override
void paint(Canvas canvas, Size size) {
final chartWidth = size.width - leftPadding;
final chartHeight = size.height - topPadding - bottomPadding;
final totalDuration = endTime - startTime;
final textPainter = TextPainter(textDirection: ui.TextDirection.ltr);
final stepValue = maxYValue / yStepCount;
// 绘制 Y 轴刻度线和文字
for (int i = 0; i <= yStepCount; i++) {
final value = stepValue * i;
final y = topPadding + chartHeight - (value / maxYValue) * chartHeight;
// 横线
// canvas.drawLine(
// Offset(leftPadding, y),
// Offset(size.width, y),
// Paint()
// ..color = Colors.grey.withOpacity(0.3)
// ..strokeWidth = 0.5,
// );
final dashPaint = Paint()
..color = Colors.grey.withOpacity(0.5)
..strokeWidth = 0.5;
drawDashedLine(
canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint);
// Y轴刻度文字
textPainter.text = TextSpan(
text: value.toStringAsFixed(0),
style: TextStyle(
fontSize: 20.rpx,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
leftPadding - textPainter.width - 4, y - textPainter.height / 2));
}
// X轴时间刻度
final startDate = DateTime.fromMillisecondsSinceEpoch(startTime);
final endDate = DateTime.fromMillisecondsSinceEpoch(endTime);
final hourStep = const Duration(hours: 1);
final xPaint = Paint()..color = Colors.grey;
final xAxisY = topPadding + chartHeight;
// 绘制整点小时刻度
for (DateTime t = startDate; t.isBefore(endDate); t = t.add(hourStep)) {
final x = ((t.millisecondsSinceEpoch - startTime) / totalDuration) *
chartWidth +
leftPadding;
final timeLabel = (t == startDate || t == endDate)
? DateFormat('HH:mm').format(t)
: DateFormat('h').format(t);
textPainter.text = TextSpan(
text: timeLabel,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(x - textPainter.width / 2, xAxisY + 4));
}
// ✅ 强制绘制结束时间刻度(确保显示)
final endX =
((endTime - startTime) / totalDuration) * chartWidth + leftPadding;
final endLabel = DateFormat('HH:mm').format(endDate);
textPainter.text = TextSpan(
text: endLabel,
style: TextStyle(
fontSize: AppConstants().smaller_text_fontSize,
color: themeController.currentColor.sc4,
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(endX - textPainter.width / 2, xAxisY + 4));
// 绘制柱子
for (final d in data) {
final left =
((d.st - startTime) / totalDuration) * chartWidth + leftPadding;
final right =
((d.et - startTime) / totalDuration) * chartWidth + leftPadding;
final barHeight = (d.value / maxYValue) * chartHeight;
final top = topPadding + chartHeight - barHeight;
final barPaint = Paint()..color = d.color;
canvas.drawRect(
Rect.fromLTRB(left, top, right, topPadding + chartHeight), barPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint,
{double dashWidth = 5, double dashSpace = 3}) {
double totalLength = (end.dx - start.dx).abs();
double dashCount = (totalLength / (dashWidth + dashSpace)).floorToDouble();
double dx = start.dx;
final dy = start.dy;
for (int i = 0; i < dashCount; i++) {
final from = Offset(dx, dy);
final to = Offset(dx + dashWidth, dy);
canvas.drawLine(from, to, paint);
dx += dashWidth + dashSpace;
}
}
}

View File

@@ -164,6 +164,114 @@ class SnoreWaveform extends StatelessWidget {
}
}
// 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")
// ..strokeWidth = 1.5
// ..style = PaintingStyle.stroke;
// final Path upperPath = Path();
// final Path lowerPath = Path();
// const double scaleY = 0.5; //波形图比例
// 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
// ..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;
// // 循环绘制整点小时标签(不包含终点)
// for (int t = startTime; t < endTime; t += hourMs) {
// double x = (t - startTime) * pixelPerMs;
// DateTime dt = DateTime.fromMillisecondsSinceEpoch(t);
// String label;
// if (t == startTime) {
// label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm
// } else {
// label = DateFormat('h').format(dt); // 中间显示小时不带前导0
// }
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 20.rpx),
// );
// }
// // 单独绘制终点时间标签,确保显示具体时分
// {
// double x = (endTime - startTime) * pixelPerMs;
// DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime);
// String label = DateFormat('HH:mm').format(dt);
// textPainter.text = TextSpan(
// text: label,
// style: TextStyle(fontSize: 10, color: Colors.grey),
// );
// textPainter.layout();
// textPainter.paint(
// canvas,
// Offset(x - textPainter.width / 2, height + 20.rpx),
// );
// }
// }
// @override
// bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
// }
class SnoreWaveformPainter extends CustomPainter {
final List<dynamic> snoreValues;
final int startTime;
@@ -184,13 +292,22 @@ class SnoreWaveformPainter extends CustomPainter {
final double pixelPerMs = width / totalDuration;
final Paint wavePaint = Paint()
..color = stringToColor("#8E7DEF")
..color = stringToColor("#8E7DEF").withOpacity(0.8)
..strokeWidth = 1.5
..style = PaintingStyle.stroke;
final Path upperPath = Path();
final Path lowerPath = Path();
const double scaleY = 0.5; //波形图比例
// ✅ 获取最大值用于自适应比例
double maxValue = snoreValues.fold<double>(0, (prev, e) {
final value = e["value"]?.toDouble() ?? 0;
return value > prev ? value : prev;
});
// ✅ 自适应缩放比例,限制波形最大高度为 height * 0.45
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"];
@@ -212,32 +329,26 @@ class SnoreWaveformPainter extends CustomPainter {
canvas.drawPath(upperPath, wavePaint);
canvas.drawPath(lowerPath, wavePaint);
// ✅ 最后绘制中心线,防止被覆盖
final Paint axisPaint = Paint()
..color = Colors.grey
..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;
// 循环绘制整点小时标签(不包含终点)
for (int t = startTime; t < endTime; t += hourMs) {
double x = (t - startTime) * pixelPerMs;
DateTime dt = DateTime.fromMillisecondsSinceEpoch(t);
String label;
if (t == startTime) {
label = DateFormat('HH:mm').format(dt); // 起点显示 HH:mm
} else {
label = DateFormat('h').format(dt); // 中间显示小时不带前导0
}
String label = t == startTime
? DateFormat('HH:mm').format(dt)
: DateFormat('h').format(dt); // 12小时制
textPainter.text = TextSpan(
text: label,
@@ -246,11 +357,11 @@ class SnoreWaveformPainter extends CustomPainter {
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 20.rpx),
Offset(x - textPainter.width / 2, height + 2), // 标签显示在底部
);
}
// 单独绘制终点时间标签,确保显示具体时分
// ✅ 画终点时间
{
double x = (endTime - startTime) * pixelPerMs;
DateTime dt = DateTime.fromMillisecondsSinceEpoch(endTime);
@@ -263,7 +374,7 @@ class SnoreWaveformPainter extends CustomPainter {
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, height + 20.rpx),
Offset(x - textPainter.width / 2, height + 2),
);
}
}