|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
|
|
|
|
|
|
|
|
import '../../report_common/widgets/chart_selection_line_overlay.dart';
|
|
|
|
import '../../report_common/widgets/health_report_subject_scope.dart';
|
|
|
|
import '../../report_common/utils/report_localization.dart';
|
|
|
|
import '../models/activity_burn_report_models.dart';
|
|
|
|
import 'activity_burn_ring.dart';
|
|
|
|
import 'activity_burn_trend_lines.dart';
|
|
|
|
|
|
|
|
class ActivityBurnWeekReportView extends StatelessWidget {
|
|
|
|
const ActivityBurnWeekReportView({
|
|
...
|
...
|
@@ -53,7 +55,8 @@ class _WeeklySummary extends StatelessWidget { |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
HealthReportSubjectScope.titleOf(context, '活动总消耗'),
|
|
|
|
HealthReportSubjectScope.titleOf(
|
|
|
|
context, context.l10n.activityTotalBurn),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._active,
|
|
|
|
fontSize: 12,
|
|
...
|
...
|
@@ -76,11 +79,11 @@ class _WeeklySummary extends StatelessWidget { |
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
const Padding(
|
|
|
|
padding: EdgeInsets.only(bottom: 5),
|
|
|
|
Padding(
|
|
|
|
padding: const EdgeInsets.only(bottom: 5),
|
|
|
|
child: Text(
|
|
|
|
'千卡',
|
|
|
|
style: TextStyle(
|
|
|
|
context.l10n.reportUnitKcal,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h1,
|
|
|
|
fontSize: 12,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
...
|
...
|
@@ -90,9 +93,9 @@ class _WeeklySummary extends StatelessWidget { |
|
|
|
],
|
|
|
|
)
|
|
|
|
else
|
|
|
|
const Text(
|
|
|
|
'等待数据',
|
|
|
|
style: TextStyle(
|
|
|
|
Text(
|
|
|
|
context.l10n.reportWaitingForData,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h1,
|
|
|
|
fontSize: 24,
|
|
|
|
fontWeight: FontWeight.w600,
|
|
...
|
...
|
@@ -103,18 +106,20 @@ class _WeeklySummary extends StatelessWidget { |
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
_SummaryMetric(
|
|
|
|
title: HealthReportSubjectScope.titleOf(context, '锻炼总时长'),
|
|
|
|
title: HealthReportSubjectScope.titleOf(
|
|
|
|
context, context.l10n.activityExerciseTotalDuration),
|
|
|
|
value: report.hasData
|
|
|
|
? report.totalExerciseMinutes.toString()
|
|
|
|
: '-',
|
|
|
|
unit: '分钟',
|
|
|
|
unit: context.l10n.reportUnitMinute,
|
|
|
|
color: ActivityBurnWeekReportView._exercise,
|
|
|
|
),
|
|
|
|
const SizedBox(width: 34),
|
|
|
|
_SummaryMetric(
|
|
|
|
title: HealthReportSubjectScope.titleOf(context, '站立总时长'),
|
|
|
|
title: HealthReportSubjectScope.titleOf(
|
|
|
|
context, context.l10n.activityStandTotalDuration),
|
|
|
|
value: report.hasData ? report.totalStandHours.toString() : '-',
|
|
|
|
unit: '小时',
|
|
|
|
unit: context.l10n.reportUnitHour,
|
|
|
|
color: ActivityBurnWeekReportView._stand,
|
|
|
|
),
|
|
|
|
],
|
|
...
|
...
|
@@ -202,7 +207,7 @@ class _RingOverviewCard extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
_RingStat(
|
|
|
|
color: ActivityBurnWeekReportView._active,
|
|
|
|
label: '完美合环',
|
|
|
|
label: context.l10n.activityPerfectRings,
|
|
|
|
value: report.perfectRingDays,
|
|
|
|
hasData: report.hasData,
|
|
|
|
report: ActivityBurnReport(
|
|
...
|
...
|
@@ -215,7 +220,7 @@ class _RingOverviewCard extends StatelessWidget { |
|
|
|
const SizedBox(width: 34),
|
|
|
|
_RingStat(
|
|
|
|
color: ActivityBurnWeekReportView._active,
|
|
|
|
label: '合上活动圆环',
|
|
|
|
label: context.l10n.activityCloseMoveRing,
|
|
|
|
value: report.activeRingDays,
|
|
|
|
hasData: report.hasData,
|
|
|
|
report: ActivityBurnReport(
|
|
...
|
...
|
@@ -230,7 +235,7 @@ class _RingOverviewCard extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
_RingStat(
|
|
|
|
color: ActivityBurnWeekReportView._exercise,
|
|
|
|
label: '合上锻炼圆环',
|
|
|
|
label: context.l10n.activityCloseExerciseRing,
|
|
|
|
value: report.exerciseRingDays,
|
|
|
|
hasData: report.hasData,
|
|
|
|
report: ActivityBurnReport(
|
|
...
|
...
|
@@ -241,7 +246,7 @@ class _RingOverviewCard extends StatelessWidget { |
|
|
|
const SizedBox(width: 34),
|
|
|
|
_RingStat(
|
|
|
|
color: ActivityBurnWeekReportView._stand,
|
|
|
|
label: '合上站立圆环',
|
|
|
|
label: context.l10n.activityCloseStandRing,
|
|
|
|
value: report.standRingDays,
|
|
|
|
hasData: report.hasData,
|
|
|
|
report: ActivityBurnReport(
|
|
...
|
...
|
@@ -313,9 +318,9 @@ class _RingStat extends StatelessWidget { |
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const TextSpan(
|
|
|
|
text: ' 天',
|
|
|
|
style: TextStyle(
|
|
|
|
TextSpan(
|
|
|
|
text: ' ${context.l10n.reportUnitDay}',
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h1,
|
|
|
|
fontSize: 14,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
...
|
...
|
@@ -335,8 +340,6 @@ class _DayRing extends StatelessWidget { |
|
|
|
|
|
|
|
final ActivityBurnReport report;
|
|
|
|
|
|
|
|
static const _weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return SizedBox(
|
|
...
|
...
|
@@ -344,7 +347,7 @@ class _DayRing extends StatelessWidget { |
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
_weekdays[report.date.weekday - 1],
|
|
|
|
reportWeekdayLabel(report.date.weekday),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h3,
|
|
|
|
fontSize: 12,
|
|
...
|
...
|
@@ -384,25 +387,27 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final maxY = _maxY;
|
|
|
|
return Container(
|
|
|
|
height: 294,
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 18, 14, 14),
|
|
|
|
height: 344,
|
|
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 14, 32),
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Colors.white,
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
HealthReportSubjectScope.titleOf(context, '热量消耗趋势'),
|
|
|
|
HealthReportSubjectScope.titleOf(
|
|
|
|
context, context.l10n.activityCalorieTrend),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h1,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
Row(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
children: [
|
|
...
|
...
|
@@ -417,9 +422,9 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
const Text(
|
|
|
|
'千卡/平均每日',
|
|
|
|
style: TextStyle(
|
|
|
|
Text(
|
|
|
|
context.l10n.activityKcalDailyAverage,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h1,
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
...
|
...
|
@@ -427,19 +432,56 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
],
|
|
|
|
),
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
Text(
|
|
|
|
widget.report.hasData ? '比上周少34%' : '比上周-',
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h2,
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
widget.report.hasData
|
|
|
|
? context.l10n.activityComparedLastWeek
|
|
|
|
: context.l10n.activityComparedUnavailable,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h2,
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const Spacer(),
|
|
|
|
_TrendLegend(
|
|
|
|
color: const Color(0xFFFFC0CE),
|
|
|
|
label: context.l10n.sleepAverage,
|
|
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
_TrendLegend(
|
|
|
|
color: const Color(0xFF96E9CB),
|
|
|
|
label: context.l10n.sleepTarget,
|
|
|
|
dashed: true,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Expanded(
|
|
|
|
child: Stack(
|
|
|
|
alignment: Alignment.center,
|
|
|
|
children: [
|
|
|
|
BarChart(_chartData()),
|
|
|
|
Positioned(
|
|
|
|
left: 0,
|
|
|
|
right: 6,
|
|
|
|
top: 0,
|
|
|
|
bottom: 42,
|
|
|
|
child: ActivityBurnTrendLines(
|
|
|
|
maxY: maxY,
|
|
|
|
average: widget.report.averageDailyActiveEnergy.toDouble(),
|
|
|
|
target: widget.report.activeEnergyGoal.toDouble(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
BarChart(_chartData(maxY)),
|
|
|
|
const Positioned(
|
|
|
|
left: 0,
|
|
|
|
right: 6,
|
|
|
|
bottom: 34,
|
|
|
|
child: SizedBox(
|
|
|
|
height: 1,
|
|
|
|
child: ActivityBurnAxisDashedLine(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
if (_touchedOffset != null)
|
|
|
|
Positioned.fill(
|
|
|
|
child: ChartSelectionLineOverlay(
|
|
...
|
...
|
@@ -450,9 +492,9 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
),
|
|
|
|
),
|
|
|
|
if (!widget.report.hasData)
|
|
|
|
const Text(
|
|
|
|
'等待数据',
|
|
|
|
style: TextStyle(
|
|
|
|
Text(
|
|
|
|
context.l10n.reportWaitingForData,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: Color(0xFFA1A0A5),
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
...
|
...
|
@@ -465,25 +507,25 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
BarChartData _chartData() {
|
|
|
|
final hasData = widget.report.hasData;
|
|
|
|
double get _maxY {
|
|
|
|
final maxValue = widget.report.days
|
|
|
|
.map((day) => day.activeEnergy?.value ?? 0)
|
|
|
|
.fold<int>(0, (max, value) => value > max ? value : max);
|
|
|
|
final maxY = ((maxValue / 100).ceil().clamp(4, 9) * 100).toDouble();
|
|
|
|
final referenceMax = [
|
|
|
|
maxValue,
|
|
|
|
widget.report.averageDailyActiveEnergy,
|
|
|
|
widget.report.activeEnergyGoal,
|
|
|
|
].reduce((max, value) => value > max ? value : max);
|
|
|
|
return ((referenceMax / 100).ceil().clamp(4, 9) * 100).toDouble();
|
|
|
|
}
|
|
|
|
|
|
|
|
BarChartData _chartData(double maxY) {
|
|
|
|
final hasData = widget.report.hasData;
|
|
|
|
return BarChartData(
|
|
|
|
minY: 0,
|
|
|
|
maxY: maxY,
|
|
|
|
gridData: FlGridData(
|
|
|
|
show: true,
|
|
|
|
drawVerticalLine: false,
|
|
|
|
horizontalInterval: 100,
|
|
|
|
getDrawingHorizontalLine: (_) => const FlLine(
|
|
|
|
color: Color(0xFFF3F3F3),
|
|
|
|
strokeWidth: 1,
|
|
|
|
dashArray: [2, 2],
|
|
|
|
),
|
|
|
|
show: false,
|
|
|
|
),
|
|
|
|
borderData: FlBorderData(show: false),
|
|
|
|
barTouchData: BarTouchData(
|
|
...
|
...
|
@@ -519,7 +561,7 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
|
|
final report = widget.report.days[groupIndex];
|
|
|
|
return BarTooltipItem(
|
|
|
|
'${report.activeEnergy?.value ?? 0}千卡\n',
|
|
|
|
'${report.activeEnergy?.value ?? 0}${context.l10n.reportUnitKcal}\n',
|
|
|
|
const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._active,
|
|
|
|
fontSize: 16,
|
|
...
|
...
|
@@ -545,16 +587,19 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
rightTitles: AxisTitles(
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
showTitles: true,
|
|
|
|
reservedSize: 28,
|
|
|
|
reservedSize: 36,
|
|
|
|
interval: 100,
|
|
|
|
getTitlesWidget: (value, meta) {
|
|
|
|
return Transform.translate(
|
|
|
|
offset: const Offset(0, -5),
|
|
|
|
child: Text(
|
|
|
|
value.toInt().toString(),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h3,
|
|
|
|
fontSize: 10,
|
|
|
|
offset: const Offset(0, -8),
|
|
|
|
child: Padding(
|
|
|
|
padding: const EdgeInsets.only(left: 8),
|
|
|
|
child: Text(
|
|
|
|
value.toInt().toString(),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h3,
|
|
|
|
fontSize: 10,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
...
|
...
|
@@ -564,16 +609,15 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
bottomTitles: AxisTitles(
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
showTitles: true,
|
|
|
|
reservedSize: 32,
|
|
|
|
reservedSize: 42,
|
|
|
|
getTitlesWidget: (value, meta) {
|
|
|
|
final index = value.toInt();
|
|
|
|
if (index < 0 || index >= widget.report.days.length) {
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
}
|
|
|
|
final day = widget.report.days[index];
|
|
|
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.only(top: 4),
|
|
|
|
padding: const EdgeInsets.only(top: 14),
|
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
Text(
|
|
...
|
...
|
@@ -584,7 +628,7 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
),
|
|
|
|
),
|
|
|
|
Text(
|
|
|
|
weekdays[day.date.weekday - 1],
|
|
|
|
reportWeekdayLabel(day.date.weekday),
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h3,
|
|
|
|
fontSize: 10,
|
|
...
|
...
|
@@ -597,22 +641,14 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
extraLinesData: ExtraLinesData(
|
|
|
|
horizontalLines: [
|
|
|
|
HorizontalLine(
|
|
|
|
y: 320,
|
|
|
|
color: ActivityBurnWeekReportView._exercise,
|
|
|
|
strokeWidth: 1,
|
|
|
|
dashArray: [2, 2],
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
baselineY: 0,
|
|
|
|
barGroups: [
|
|
|
|
for (var i = 0; i < widget.report.days.length; i++)
|
|
|
|
BarChartGroupData(
|
|
|
|
x: i,
|
|
|
|
barRods: [
|
|
|
|
BarChartRodData(
|
|
|
|
fromY: 0,
|
|
|
|
toY:
|
|
|
|
(widget.report.days[i].activeEnergy?.value ?? 0).toDouble(),
|
|
|
|
width: 16,
|
|
...
|
...
|
@@ -628,7 +664,50 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> { |
|
|
|
}
|
|
|
|
|
|
|
|
String _tooltipDate(DateTime date) {
|
|
|
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
|
|
|
return '${DateFormat('M月d日').format(date)} 星期${weekdays[date.weekday - 1]}';
|
|
|
|
return l10n.reportDateWithWeekday(
|
|
|
|
reportMonthDay(date),
|
|
|
|
reportWeekdayLabel(date.weekday),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _TrendLegend extends StatelessWidget {
|
|
|
|
const _TrendLegend({
|
|
|
|
required this.color,
|
|
|
|
required this.label,
|
|
|
|
this.dashed = false,
|
|
|
|
});
|
|
|
|
|
|
|
|
final Color color;
|
|
|
|
final String label;
|
|
|
|
final bool dashed;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Row(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
SizedBox(
|
|
|
|
width: 19,
|
|
|
|
child: Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
children: dashed
|
|
|
|
? List.generate(
|
|
|
|
5,
|
|
|
|
(_) => Container(width: 2, height: 2, color: color),
|
|
|
|
)
|
|
|
|
: [Container(width: 19, height: 2, color: color)],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
Text(
|
|
|
|
label,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: ActivityBurnWeekReportView._h2,
|
|
|
|
fontSize: 12,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|