|
...
|
...
|
@@ -8,6 +8,7 @@ import '../../report_common/utils/report_localization.dart'; |
|
|
|
import '../../report_common/widgets/chart_selection_line_overlay.dart';
|
|
|
|
import '../../report_common/widgets/health_report_subject_scope.dart';
|
|
|
|
import '../models/hrv_report_models.dart';
|
|
|
|
import 'hrv_distribution_bar.dart';
|
|
|
|
|
|
|
|
class HrvWeekReportView extends StatelessWidget {
|
|
|
|
const HrvWeekReportView({
|
|
...
|
...
|
@@ -643,58 +644,90 @@ class _DistributionCard extends StatelessWidget { |
|
|
|
final HrvPeriodReport report;
|
|
|
|
final bool showExample;
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
static const _cardHeight = 425.0;
|
|
|
|
static const _barRight = 45.0;
|
|
|
|
static const _barWidth = 42.0;
|
|
|
|
static const _barHeight = 220.0;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return _Card(
|
|
|
|
onTap: onTap,
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
_ReportTitle(
|
|
|
|
title: HealthReportSubjectScope.titleOf(
|
|
|
|
context,
|
|
|
|
context.l10n.hrvStressDistribution,
|
|
|
|
),
|
|
|
|
showExample: showExample,
|
|
|
|
),
|
|
|
|
const SizedBox(height: 17),
|
|
|
|
Row(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(context.l10n.hrvValidDays,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: Color(0xFFB0B0B6), fontSize: 11)),
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
_ValueWithUnit(
|
|
|
|
value: '${report.validDays}',
|
|
|
|
unit: context.l10n.reportUnitDay,
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: const BoxConstraints(minHeight: _cardHeight),
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
_ReportTitle(
|
|
|
|
title: HealthReportSubjectScope.titleOf(
|
|
|
|
context,
|
|
|
|
context.l10n.hrvStressDistribution,
|
|
|
|
),
|
|
|
|
showExample: showExample,
|
|
|
|
fontSize: 16,
|
|
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
Text(
|
|
|
|
context.l10n.hrvValidDays,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: Color(0xFFB0B0B6),
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
|
|
const SizedBox(height: 18),
|
|
|
|
_DistributionGrid(report: report),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
_ValueWithUnit(
|
|
|
|
value: '${report.validDays}',
|
|
|
|
unit: context.l10n.reportUnitDay,
|
|
|
|
),
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
SizedBox(
|
|
|
|
width: 216,
|
|
|
|
child: _DistributionGrid(report: report),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
const Divider(height: 1, color: Color(0xFFF3F3F3)),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: _ExtremeHrv(
|
|
|
|
title: context.l10n.hrvLowest,
|
|
|
|
day: report.minDay,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: _ExtremeHrv(
|
|
|
|
title: context.l10n.hrvHighest,
|
|
|
|
day: report.maxDay,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned(
|
|
|
|
top: 0,
|
|
|
|
right: _barRight,
|
|
|
|
width: _barWidth,
|
|
|
|
child: Container(
|
|
|
|
alignment: Alignment.topCenter,
|
|
|
|
padding: EdgeInsets.only(top: 50.h),
|
|
|
|
child: SizedBox(
|
|
|
|
height: _barHeight,
|
|
|
|
child: IgnorePointer(
|
|
|
|
child: _DistributionBar(report: report),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
_DistributionBar(report: report),
|
|
|
|
const SizedBox(width: 20),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const Divider(height: 29, color: Color(0xFFF3F3F3)),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: _ExtremeHrv(
|
|
|
|
title: context.l10n.hrvLowest, day: report.minDay)),
|
|
|
|
Expanded(
|
|
|
|
child: _ExtremeHrv(
|
|
|
|
title: context.l10n.hrvHighest, day: report.maxDay)),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
...
|
...
|
@@ -710,14 +743,13 @@ class _DistributionGrid extends StatelessWidget { |
|
|
|
return LayoutBuilder(
|
|
|
|
builder: (context, constraints) {
|
|
|
|
const spacing = 16.0;
|
|
|
|
final itemWidth = (constraints.maxWidth - spacing) / 2;
|
|
|
|
return Wrap(
|
|
|
|
spacing: spacing,
|
|
|
|
runSpacing: 13,
|
|
|
|
runSpacing: 16,
|
|
|
|
children: [
|
|
|
|
for (final level in HrvStressLevel.values)
|
|
|
|
SizedBox(
|
|
|
|
width: itemWidth,
|
|
|
|
width: 100,
|
|
|
|
child: _DistributionItem(level: level, report: report),
|
|
|
|
),
|
|
|
|
],
|
|
...
|
...
|
@@ -743,24 +775,25 @@ class _DistributionItem extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
Row(children: [
|
|
|
|
Container(
|
|
|
|
width: 9,
|
|
|
|
height: 9,
|
|
|
|
width: 12,
|
|
|
|
height: 12,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Color(level.colorValue), shape: BoxShape.circle)),
|
|
|
|
const SizedBox(width: 3),
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
Expanded(
|
|
|
|
child: Text(
|
|
|
|
level.label,
|
|
|
|
maxLines: 2,
|
|
|
|
maxLines: 1,
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
style: const TextStyle(
|
|
|
|
fontSize: 10,
|
|
|
|
fontSize: 12,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.2,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
const SizedBox(height: 3),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Text(
|
|
|
|
report.hasData
|
|
|
|
? '$count${context.l10n.reportUnitDay}'
|
|
...
|
...
|
@@ -769,10 +802,16 @@ class _DistributionItem extends StatelessWidget { |
|
|
|
color: report.hasData
|
|
|
|
? const Color(0xFF0F0F11)
|
|
|
|
: const Color(0xFFB0B0B6),
|
|
|
|
fontSize: report.hasData ? 20 : 14,
|
|
|
|
fontWeight: FontWeight.w500)),
|
|
|
|
fontSize: report.hasData ? 20 : 18,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.2)),
|
|
|
|
const SizedBox(height: 1),
|
|
|
|
Text('$percent%',
|
|
|
|
style: const TextStyle(color: Color(0xFFB0B0B6), fontSize: 10)),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: Color(0xFFB0B0B6),
|
|
|
|
fontSize: 12,
|
|
|
|
height: 1.2,
|
|
|
|
)),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
...
|
...
|
@@ -785,170 +824,22 @@ class _DistributionBar extends StatelessWidget { |
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
const radius = 7.0;
|
|
|
|
final segments = [
|
|
|
|
for (final level in HrvStressLevel.values)
|
|
|
|
if (report.countFor(level) > 0)
|
|
|
|
_DistributionBarSegment(
|
|
|
|
HrvDistributionBarSegment(
|
|
|
|
color: Color(level.colorValue),
|
|
|
|
flex: report.countFor(level),
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final hasSegments = report.hasData && segments.isNotEmpty;
|
|
|
|
final height = hasSegments
|
|
|
|
? _DistributionBarPainter.visibleHeightFor(segments)
|
|
|
|
: 180.0;
|
|
|
|
final paintSize = Size(44.w, height);
|
|
|
|
|
|
|
|
return SizedBox.fromSize(
|
|
|
|
size: const Size(33, 180),
|
|
|
|
child: OverflowBox(
|
|
|
|
alignment: Alignment.bottomLeft,
|
|
|
|
minWidth: paintSize.width,
|
|
|
|
maxWidth: paintSize.width,
|
|
|
|
minHeight: paintSize.height,
|
|
|
|
maxHeight: paintSize.height,
|
|
|
|
child: Transform.translate(
|
|
|
|
offset: const Offset(0, -12),
|
|
|
|
child: SizedBox.fromSize(
|
|
|
|
size: paintSize,
|
|
|
|
child: CustomPaint(
|
|
|
|
painter: _DistributionBarPainter(
|
|
|
|
segments: hasSegments ? segments : const [],
|
|
|
|
backgroundColor: const Color(0xFFF3F3F3),
|
|
|
|
radius: radius,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
return HrvDistributionBar(
|
|
|
|
segments: report.hasData ? segments : const [],
|
|
|
|
backgroundColor: const Color(0xFFF3F3F3),
|
|
|
|
radius: 8,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DistributionBarSegment {
|
|
|
|
const _DistributionBarSegment({required this.color, required this.flex});
|
|
|
|
|
|
|
|
final Color color;
|
|
|
|
final int flex;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DistributionBarPainter extends CustomPainter {
|
|
|
|
const _DistributionBarPainter({
|
|
|
|
required this.segments,
|
|
|
|
required this.backgroundColor,
|
|
|
|
required this.radius,
|
|
|
|
});
|
|
|
|
|
|
|
|
final List<_DistributionBarSegment> segments;
|
|
|
|
final Color backgroundColor;
|
|
|
|
final double radius;
|
|
|
|
static const maxVisibleHeight = 220.0;
|
|
|
|
static const _borderWidth = 1.0;
|
|
|
|
static const _overlapHeight = 16.0;
|
|
|
|
static const _weekDayCount = 7;
|
|
|
|
|
|
|
|
static double visibleHeightFor(List<_DistributionBarSegment> segments) {
|
|
|
|
if (segments.isEmpty) return 0;
|
|
|
|
final totalFlex =
|
|
|
|
segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
return maxVisibleHeight * totalFlex / _weekDayCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
final clip = RRect.fromRectAndRadius(
|
|
|
|
Offset.zero & size,
|
|
|
|
Radius.circular(radius),
|
|
|
|
);
|
|
|
|
canvas.save();
|
|
|
|
canvas.clipRRect(clip);
|
|
|
|
|
|
|
|
final total = segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
if (total == 0) {
|
|
|
|
canvas.drawRRect(
|
|
|
|
clip,
|
|
|
|
Paint()
|
|
|
|
..color = backgroundColor
|
|
|
|
..style = PaintingStyle.fill,
|
|
|
|
);
|
|
|
|
canvas.restore();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final visibleHeights = _visibleSegmentHeightsFor(size.height, segments);
|
|
|
|
var bottom = 0.0;
|
|
|
|
for (var index = 0; index < segments.length; index++) {
|
|
|
|
final segment = segments[index];
|
|
|
|
final isBottomSegment = index == segments.length - 1;
|
|
|
|
final drawTop = index == 0 ? 0.0 : bottom - _overlapHeight;
|
|
|
|
final drawBottom = drawTop +
|
|
|
|
visibleHeights[index] +
|
|
|
|
(isBottomSegment ? 0 : _overlapHeight);
|
|
|
|
_drawLayeredSegment(
|
|
|
|
canvas,
|
|
|
|
Rect.fromLTRB(0, drawTop, size.width, drawBottom),
|
|
|
|
color: segment.color,
|
|
|
|
radius: radius,
|
|
|
|
);
|
|
|
|
bottom = drawBottom;
|
|
|
|
}
|
|
|
|
canvas.restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<double> _visibleSegmentHeightsFor(
|
|
|
|
double visibleHeight,
|
|
|
|
List<_DistributionBarSegment> segments,
|
|
|
|
) {
|
|
|
|
final totalFlex =
|
|
|
|
segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
final dayHeight = visibleHeight / totalFlex;
|
|
|
|
return [
|
|
|
|
for (final segment in segments) segment.flex * dayHeight,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
void _drawLayeredSegment(
|
|
|
|
Canvas canvas,
|
|
|
|
Rect rect, {
|
|
|
|
required Color color,
|
|
|
|
required double radius,
|
|
|
|
}) {
|
|
|
|
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(radius));
|
|
|
|
canvas.drawRRect(
|
|
|
|
rrect,
|
|
|
|
Paint()
|
|
|
|
..color = color
|
|
|
|
..style = PaintingStyle.fill,
|
|
|
|
);
|
|
|
|
canvas.drawRRect(
|
|
|
|
rrect.deflate(_borderWidth / 2),
|
|
|
|
Paint()
|
|
|
|
..color = Colors.white
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
..strokeWidth = _borderWidth,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRepaint(covariant _DistributionBarPainter oldDelegate) {
|
|
|
|
if (backgroundColor != oldDelegate.backgroundColor ||
|
|
|
|
radius != oldDelegate.radius ||
|
|
|
|
segments.length != oldDelegate.segments.length) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
for (var i = 0; i < segments.length; i++) {
|
|
|
|
final segment = segments[i];
|
|
|
|
final oldSegment = oldDelegate.segments[i];
|
|
|
|
if (segment.color != oldSegment.color ||
|
|
|
|
segment.flex != oldSegment.flex) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ExtremeHrv extends StatelessWidget {
|
|
|
|
const _ExtremeHrv({required this.title, required this.day});
|
|
|
|
|
|
...
|
...
|
@@ -961,15 +852,15 @@ class _ExtremeHrv extends StatelessWidget { |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(title,
|
|
|
|
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500)),
|
|
|
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
|
|
|
|
const SizedBox(height: 5),
|
|
|
|
_ValueWithUnit(
|
|
|
|
value: day == null ? '-' : '${day!.averageHrv!.round()}',
|
|
|
|
unit: 'ms'),
|
|
|
|
const SizedBox(height: 3),
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
Text(
|
|
|
|
day == null ? '-月-日' : reportMonthDay(day!.date),
|
|
|
|
style: const TextStyle(color: Color(0xFFB0B0B6), fontSize: 10),
|
|
|
|
style: const TextStyle(color: Color(0xFFB0B0B6), fontSize: 12),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
...
|
...
|
@@ -989,10 +880,10 @@ class _ValueWithUnit extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
Text(value,
|
|
|
|
style: const TextStyle(
|
|
|
|
fontSize: 25, fontWeight: FontWeight.w600, height: 1)),
|
|
|
|
fontSize: 26, fontWeight: FontWeight.w600, height: 1)),
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.only(left: 3, bottom: 2),
|
|
|
|
child: Text(unit, style: const TextStyle(fontSize: 10)),
|
|
|
|
child: Text(unit, style: const TextStyle(fontSize: 12)),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
...
|
...
|
@@ -1000,10 +891,15 @@ class _ValueWithUnit extends StatelessWidget { |
|
|
|
}
|
|
|
|
|
|
|
|
class _ReportTitle extends StatelessWidget {
|
|
|
|
const _ReportTitle({required this.title, required this.showExample});
|
|
|
|
const _ReportTitle({
|
|
|
|
required this.title,
|
|
|
|
required this.showExample,
|
|
|
|
this.fontSize = 15,
|
|
|
|
});
|
|
|
|
|
|
|
|
final String title;
|
|
|
|
final bool showExample;
|
|
|
|
final double fontSize;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
...
|
...
|
@@ -1011,7 +907,7 @@ class _ReportTitle extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
showExample ? context.l10n.reportExampleTitle(title) : title,
|
|
|
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
|
|
|
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600),
|
|
|
|
),
|
|
|
|
if (showExample) ...[
|
|
|
|
Image.asset(
|
...
|
...
|
|