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(double.infinity, 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 = stepValue * i; final y = topPadding + chartHeight - (value / maxYValue) * chartHeight; final dashPaint = Paint() ..color = Colors.grey.withOpacity(0.5) ..strokeWidth = 0.5; drawDashedLine( canvas, Offset(leftPadding, y), Offset(size.width, y), dashPaint); 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 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)); // 柱子绘制 & 提示信息缓存 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; tipOffset = Offset(tipLeft, tipTop); 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; } } }