Commit 7cccaa07bfc0932d8ecad04060d44dcc6e9cd418

Authored by 刘宏哲
1 parent 98c7d845

feat(app): sleep report add default sleep target

... ... @@ -256,7 +256,7 @@ class _ActivityBurnRingPainter extends CustomPainter {
if (progress <= 0) return;
final rect = Rect.fromCircle(center: center, radius: radius);
final safeProgress = math.max(0, progress);
final safeProgress = math.max(0.0, progress);
final hasCompletedLap = safeProgress >= 1;
final remainder = safeProgress % 1;
final visibleProgress = hasCompletedLap ? remainder : safeProgress;
... ... @@ -266,7 +266,7 @@ class _ActivityBurnRingPainter extends CustomPainter {
..color = progressColor
..style = PaintingStyle.stroke
..strokeWidth = width
..strokeCap = StrokeCap.round;
..strokeCap = StrokeCap.butt;
final fullLapPaint = Paint()
..color = progressColor
... ... @@ -286,7 +286,14 @@ class _ActivityBurnRingPainter extends CustomPainter {
);
}
final endAngle = startAngle + math.pi * 2 * visibleProgress;
final endAngle = isExactLap
? startAngle
: _endCapAngle(
startAngle: startAngle,
radius: radius,
width: width,
progress: visibleProgress,
);
// Head layer: its shadow is painted first, then the colored current lap.
// For an exact lap there is no partial arc, so paint a colored head at the
... ... @@ -309,16 +316,71 @@ class _ActivityBurnRingPainter extends CustomPainter {
color: progressColor,
);
} else {
canvas.drawArc(
rect,
startAngle,
math.pi * 2 * visibleProgress,
false,
progressPaint,
_drawPartialRingWithRoundedCaps(
canvas,
rect: rect,
startAngle: startAngle,
radius: radius,
progress: visibleProgress,
paint: progressPaint,
);
}
}
/// Matches the home-page ring: shrink the butt-capped arc, then add two
/// circular caps so that rounded ends never consume the remaining gap.
void _drawPartialRingWithRoundedCaps(
Canvas canvas, {
required Rect rect,
required double startAngle,
required double radius,
required double progress,
required Paint paint,
}) {
final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
if (sweep <= 0) return;
final capAngle = math.asin((paint.strokeWidth / 2) / radius);
final actualCapAngle = math.min(capAngle, sweep / 2).toDouble();
final arcStart = startAngle + actualCapAngle;
final arcSweep = sweep - actualCapAngle * 2;
if (arcSweep > 0) {
canvas.drawArc(rect, arcStart, arcSweep, false, paint);
}
final capPaint = Paint()
..color = paint.color
..style = PaintingStyle.fill;
final center = rect.center;
canvas.drawCircle(
_pointOnRing(center, radius, arcStart),
paint.strokeWidth / 2,
capPaint,
);
canvas.drawCircle(
_pointOnRing(center, radius, arcStart + arcSweep),
paint.strokeWidth / 2,
capPaint,
);
}
double _endCapAngle({
required double startAngle,
required double radius,
required double width,
required double progress,
}) {
final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
final capAngle = math.asin((width / 2) / radius);
return startAngle + sweep - math.min(capAngle, sweep / 2).toDouble();
}
Offset _pointOnRing(Offset center, double radius, double angle) => Offset(
center.dx + math.cos(angle) * radius,
center.dy + math.sin(angle) * radius,
);
void _drawRingEndCap(
Canvas canvas, {
required Offset center,
... ...
... ... @@ -27,6 +27,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
static const _weekDateRangeType = 0;
static const _monthDateRangeType = 1;
static const _dayDateRangeType = 3;
static const _defaultSleepTargetDurationSeconds = 8 * 60 * 60;
final HealthDataSource _healthDataSource;
... ... @@ -95,7 +96,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
SleepReport.empty(start.add(Duration(days: i))),
],
averageDurationSeconds: data.avgSleepDuration,
targetDurationSeconds: data.sleepTargetDuration,
targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
averageScore: data.avgSleepScore,
averageSleepEvaluate: data.avgSleepEvaluate?.toInt(),
previousAverageDurationSeconds: data.qoqAvgSleepDuration,
... ... @@ -140,7 +141,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
SleepReport.empty(start.add(Duration(days: i))),
],
averageDurationSeconds: data.avgSleepDuration,
targetDurationSeconds: data.sleepTargetDuration,
targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
averageScore: data.avgSleepScore,
averageSleepEvaluate: data.avgSleepEvaluate?.toInt(),
previousAverageDurationSeconds: data.qoqAvgSleepDuration,
... ... @@ -280,7 +281,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
asleepTime,
points,
sleepDurationSeconds: dailyTrend.totalTime,
targetDurationSeconds: data.sleepTargetDuration,
targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
averageBpm: data.avgHr?.toInt(),
maxBpm: data.maxHr?.toInt(),
minBpm: data.minHr?.toInt(),
... ... @@ -378,6 +379,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
);
}
num _targetDurationSeconds(num? value) =>
value == null || value <= 0 ? _defaultSleepTargetDurationSeconds : value;
SleepHeartRateSummary _heartRateSummary(
List<SleepHeartRatePoint> points,
DateTime? sleepStart,
... ...
... ... @@ -157,33 +157,81 @@ class _SleepQualityRingPainter extends CustomPainter {
..color = _purple
..style = PaintingStyle.stroke
..strokeWidth = 22
..strokeCap = StrokeCap.round;
..strokeCap = StrokeCap.butt;
final innerProgress = Paint()
..color = _blue
..style = PaintingStyle.stroke
..strokeWidth = 22
..strokeCap = StrokeCap.round;
..strokeCap = StrokeCap.butt;
if (hasDurationTarget) {
canvas.drawArc(
outerRect,
startAngle,
math.pi * 2 * durationProgress,
false,
outerProgress,
_drawProgressArc(
canvas,
rect: outerRect,
startAngle: startAngle,
progress: durationProgress,
paint: outerProgress,
);
}
if (hasQuality) {
canvas.drawArc(
innerRect,
startAngle,
math.pi * 2 * qualityProgress,
false,
innerProgress,
_drawProgressArc(
canvas,
rect: innerRect,
startAngle: startAngle,
progress: qualityProgress,
paint: innerProgress,
);
}
}
/// Draws rounded ends without allowing them to consume the remaining gap.
/// This preserves the visual progress ratio for near-complete circles.
void _drawProgressArc(
Canvas canvas, {
required Rect rect,
required double startAngle,
required double progress,
required Paint paint,
}) {
final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
if (sweep <= 0) return;
if (sweep >= math.pi * 2) {
canvas.drawArc(rect, startAngle, math.pi * 2, false, paint);
return;
}
final radius = rect.width / 2;
final capAngle = math.asin((paint.strokeWidth / 2) / radius);
final actualCapAngle = math.min(capAngle, sweep / 2).toDouble();
final arcStart = startAngle + actualCapAngle;
final arcSweep = sweep - actualCapAngle * 2;
final center = rect.center;
if (arcSweep > 0) {
canvas.drawArc(rect, arcStart, arcSweep, false, paint);
}
final capPaint = Paint()
..color = paint.color
..style = PaintingStyle.fill;
canvas.drawCircle(
_pointOnCircle(center, radius, arcStart),
paint.strokeWidth / 2,
capPaint,
);
canvas.drawCircle(
_pointOnCircle(center, radius, arcStart + arcSweep),
paint.strokeWidth / 2,
capPaint,
);
}
Offset _pointOnCircle(Offset center, double radius, double angle) => Offset(
center.dx + radius * math.cos(angle),
center.dy + radius * math.sin(angle),
);
@override
bool shouldRepaint(covariant _SleepQualityRingPainter oldDelegate) {
return oldDelegate.durationProgress != durationProgress ||
... ...