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 StatefulWidget { final List 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 State createState() => _BarChartWidgetState(); } class _BarChartWidgetState extends State { BarData? selectedBar; void _handleTapOrDrag(Offset localPosition, Size size) { final chartWidth = size.width - 30.rpx; final totalDuration = widget.endTime - widget.startTime; for (final d in widget.data) { final left = ((d.st - widget.startTime) / totalDuration) * chartWidth + 30.rpx; final right = ((d.et - widget.startTime) / totalDuration) * chartWidth + 30.rpx; if (localPosition.dx >= left && localPosition.dx <= right) { setState(() { selectedBar = d; }); return; } } setState(() { selectedBar = null; }); } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanDown: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), onPanUpdate: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), onTapDown: (details) => _handleTapOrDrag(details.localPosition, constraints.biggest), child: CustomPaint( size: Size(constraints.maxWidth, 250.rpx), // 使用约束的最大宽度 painter: BarChartPainter( widget.data, widget.startTime, widget.endTime, maxYValue: widget.maxYValue, yStepCount: widget.yStepCount, selectedBar: selectedBar, ), ), ); }); } } class BarChartPainter extends CustomPainter { final List data; final int startTime; final int endTime; final double maxYValue; final int yStepCount; final BarData? selectedBar; final double topPadding = 0; final double bottomPadding = 0; final double leftPadding = 30.rpx; BarChartPainter( this.data, this.startTime, this.endTime, { required this.maxYValue, this.yStepCount = 5, this.selectedBar, }); @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; final dashPaint = Paint() ..color = Colors.grey.withOpacity(0.4) ..strokeWidth = 1.rpx; drawDashedLine( canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); textPainter.text = TextSpan( text: value.toStringAsFixed(0), style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), ); } // X轴刻度 - 参考横线图的24小时制 final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 // 计算总小时数 final totalHours = endDate.difference(startDate).inHours + 1; final startHour = startDate.hour; // 绘制X轴主线(实线) final xAxisPaint = Paint() ..color = Colors.grey.withOpacity(0.4) ..strokeWidth = 1.rpx; canvas.drawLine( Offset(leftPadding, xAxisY), Offset(size.width, xAxisY), xAxisPaint, ); // 绘制左右两侧时间标签(HH:mm格式) final leftLabel = DateFormat('HH:mm').format(startDate); textPainter.text = TextSpan( text: leftLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); final rightLabel = DateFormat('HH:mm').format(endDate); textPainter.text = TextSpan( text: rightLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); // 绘制中间小时刻度(只显示小时数字) for (int i = 1; i < totalHours; i++) { final double x = leftPadding + chartWidth * i / totalHours; int hourLabelNum = (startHour + i) % 24; final hourLabel = '$hourLabelNum'; textPainter.text = TextSpan( text: hourLabel, style: TextStyle( fontSize: 18.rpx, color: themeController.currentColor.sc4, ), ); textPainter.layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); } // 柱子绘制 & 提示信息缓存 Offset? tipOffset; Size? tipSize; String? tipText; Color tipColor = Colors.black; 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, ); // 缓存 tip 信息 if (selectedBar == d) { tipText = '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; final tp = TextPainter( text: TextSpan( text: tipText, style: TextStyle(fontSize: 16.rpx, color: Colors.white), ), textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); tp.layout(); final tipWidth = tp.width + 20.rpx; final tipHeight = tp.height + 10.rpx; final tipLeft = left + (right - left) / 2 - tipWidth / 2; final tipTop = top - tipHeight - 10.rpx; // 确保tip不会超出画布顶部 final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; tipOffset = Offset(tipLeft, adjustedTipTop); tipSize = Size(tipWidth, tipHeight); tipColor = Colors.black.withOpacity(0.8); } } // 绘制 tip(在柱子之上) if (tipText != null && tipOffset != null && tipSize != null) { final rect = RRect.fromRectAndRadius( Rect.fromLTWH( tipOffset.dx, tipOffset.dy, tipSize.width, tipSize.height), Radius.circular(8.rpx), ); final tipBgPaint = Paint()..color = tipColor; canvas.drawRRect(rect, tipBgPaint); final tipTextPainter = TextPainter( text: TextSpan( text: tipText, style: TextStyle(fontSize: 16.rpx, color: Colors.white), ), textAlign: TextAlign.center, textDirection: ui.TextDirection.ltr, ); tipTextPainter.layout(); tipTextPainter.paint( canvas, Offset(tipOffset.dx + 10.rpx, tipOffset.dy + 5.rpx), ); } } @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; } } } //横线显示 // import 'dart:math'; // import 'dart:ui' as ui; // 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'; // 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 StatefulWidget { // final List 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 // State createState() => _BarChartWidgetState(); // } // class _BarChartWidgetState extends State { // BarData? selectedBar; // void _handleTapOrDrag(Offset localPosition, Size size) { // print('点击位置: $localPosition, 画布大小: $size'); // final chartWidth = size.width - 30.rpx; // final totalDuration = widget.endTime - widget.startTime; // // 使用与绘制相同的chartHeight计算方式 // final double topPadding = 0; // final double bottomPadding = 0; // final double leftPadding = 30.rpx; // final double chartHeight = size.height - topPadding - bottomPadding; // for (final d in widget.data) { // final left = ((d.st - widget.startTime) / totalDuration) * chartWidth + // leftPadding; // final right = ((d.et - widget.startTime) / totalDuration) * chartWidth + // leftPadding; // // 使用与绘制相同的Y坐标计算方式 // final y = topPadding + chartHeight * (1 - d.value / widget.maxYValue); // // 判断点击是否在横线附近(增加容差范围) // if (localPosition.dx >= left - 5.rpx && // localPosition.dx <= right + 5.rpx && // (localPosition.dy - y).abs() < 15.rpx) { // setState(() { // selectedBar = d; // }); // return; // } // } // setState(() { // selectedBar = null; // }); // } // @override // Widget build(BuildContext context) { // return LayoutBuilder(builder: (context, constraints) { // return GestureDetector( // behavior: HitTestBehavior.opaque, // onPanDown: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // onPanUpdate: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // onTapDown: (details) => // _handleTapOrDrag(details.localPosition, constraints.biggest), // child: CustomPaint( // // size: Size(double.infinity, 500.rpx), // size: Size(constraints.maxWidth, 500.rpx), // 使用约束的最大宽度 // painter: BarChartPainter( // widget.data, // widget.startTime, // widget.endTime, // maxYValue: widget.maxYValue, // yStepCount: widget.yStepCount, // selectedBar: selectedBar, // ), // ), // ); // }); // } // } // class BarChartPainter extends CustomPainter { // final List data; // final int startTime; // final int endTime; // final double maxYValue; // final int yStepCount; // final BarData? selectedBar; // final double topPadding = 0; // final double bottomPadding = 0; // final double leftPadding = 30.rpx; // BarChartPainter( // this.data, // this.startTime, // this.endTime, { // required this.maxYValue, // this.yStepCount = 5, // this.selectedBar, // }); // @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 = maxYValue - (stepValue * i); // 从最大值开始递减 // final y = topPadding + chartHeight * i / yStepCount; // final dashPaint = Paint() // ..color = Colors.grey.withOpacity(0.4) // ..strokeWidth = 1.rpx; // // 最上面的线(i == ySegments)是实线 // if (i == yStepCount) { // canvas.drawLine( // Offset(leftPadding, y), Offset(size.width, y), dashPaint); // } else { // // 其他线都是虚线 // drawDashedLine( // canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); // } // textPainter.text = TextSpan( // text: value.toStringAsFixed(0), // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, // Offset(leftPadding - textPainter.width - 4, y - textPainter.height / 2), // ); // } // // X轴刻度 - 24小时制 // final startDate = DateTime.fromMillisecondsSinceEpoch(startTime); // final endDate = DateTime.fromMillisecondsSinceEpoch(endTime); // final xAxisY = topPadding + chartHeight; // 这是最底部的Y坐标 // // 计算总小时数 // final totalHours = endDate.difference(startDate).inHours + 1; // final startHour = startDate.hour; // // 绘制X轴主线(实线) // final xAxisPaint = Paint() // ..color = Colors.grey.withOpacity(0.4) // ..strokeWidth = 1.rpx; // canvas.drawLine( // Offset(leftPadding, xAxisY), // Offset(size.width, xAxisY), // xAxisPaint, // ); // // 绘制左右两侧时间标签 // final leftLabel = DateFormat('HH:mm').format(startDate); // textPainter.text = TextSpan( // text: leftLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(leftPadding - textPainter.width / 2, xAxisY + 8.rpx)); // final rightLabel = DateFormat('HH:mm').format(endDate); // textPainter.text = TextSpan( // text: rightLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(size.width - textPainter.width / 2, xAxisY + 8.rpx)); // // 绘制中间小时刻度 // for (int i = 1; i < totalHours; i++) { // final double x = leftPadding + chartWidth * i / totalHours; // int hourLabelNum = (startHour + i) % 24; // final hourLabel = '$hourLabelNum'; // textPainter.text = TextSpan( // text: hourLabel, // style: TextStyle( // fontSize: 18.rpx, // color: themeController.currentColor.sc4, // ), // ); // textPainter.layout(); // textPainter.paint( // canvas, Offset(x - textPainter.width / 2, xAxisY + 8.rpx)); // } // // 绘制数据横线(根据数据值绘制水平线段) // for (final d in data) { // final left = // ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; // final right = // ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; // // 根据value计算Y位置(0在底部,maxYValue在顶部) // final y = topPadding + chartHeight * (1 - d.value / maxYValue); // final linePaint = Paint() // ..style = PaintingStyle.stroke // ..strokeWidth = 3.rpx // ..color = d.color // ..strokeCap = StrokeCap.round; // // 画水平线段 // canvas.drawLine(Offset(left, y), Offset(right, y), linePaint); // } // // 如果选中了某条横线,显示提示信息 // // 如果选中了某条横线,显示提示信息 // if (selectedBar != null) { // final d = selectedBar!; // final left = // ((d.st - startTime) / totalDuration) * chartWidth + leftPadding; // final right = // ((d.et - startTime) / totalDuration) * chartWidth + leftPadding; // final y = topPadding + chartHeight * (1 - d.value / maxYValue); // final tipText = // '${d.name}\n${d.value.toStringAsFixed(1)}次\n${MyUtils.formatToHHmm(d.st)}'; // final tp = TextPainter( // text: TextSpan( // text: tipText, // style: TextStyle(fontSize: 16.rpx, color: Colors.white), // ), // textAlign: TextAlign.center, // textDirection: ui.TextDirection.ltr, // ); // tp.layout(); // final tipWidth = tp.width + 20.rpx; // final tipHeight = tp.height + 10.rpx; // final tipLeft = left + (right - left) / 2 - tipWidth / 2; // final tipTop = y - tipHeight - 10.rpx; // // 确保tip不会超出画布顶部 - 明确转换为double // final double adjustedTipTop = tipTop < 0 ? 0.0 : tipTop; // // 绘制tip背景 // final rect = RRect.fromRectAndRadius( // Rect.fromLTWH(tipLeft, adjustedTipTop, tipWidth, tipHeight), // Radius.circular(8.rpx), // ); // final tipBgPaint = Paint()..color = Colors.black.withOpacity(0.8); // canvas.drawRRect(rect, tipBgPaint); // // 绘制tip文字 // tp.paint( // canvas, // Offset(tipLeft + 10.rpx, adjustedTipTop + 5.rpx), // ); // } // } // @override // bool shouldRepaint(covariant CustomPainter oldDelegate) => true; // void drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint, // {double dashWidth = 4, double dashSpace = 3}) { // 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; // } // } // }