Commit 7cccaa07bfc0932d8ecad04060d44dcc6e9cd418

Authored by 刘宏哲
1 parent 98c7d845

feat(app): sleep report add default sleep target

@@ -256,7 +256,7 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -256,7 +256,7 @@ class _ActivityBurnRingPainter extends CustomPainter {
256 if (progress <= 0) return; 256 if (progress <= 0) return;
257 257
258 final rect = Rect.fromCircle(center: center, radius: radius); 258 final rect = Rect.fromCircle(center: center, radius: radius);
259 - final safeProgress = math.max(0, progress); 259 + final safeProgress = math.max(0.0, progress);
260 final hasCompletedLap = safeProgress >= 1; 260 final hasCompletedLap = safeProgress >= 1;
261 final remainder = safeProgress % 1; 261 final remainder = safeProgress % 1;
262 final visibleProgress = hasCompletedLap ? remainder : safeProgress; 262 final visibleProgress = hasCompletedLap ? remainder : safeProgress;
@@ -266,7 +266,7 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -266,7 +266,7 @@ class _ActivityBurnRingPainter extends CustomPainter {
266 ..color = progressColor 266 ..color = progressColor
267 ..style = PaintingStyle.stroke 267 ..style = PaintingStyle.stroke
268 ..strokeWidth = width 268 ..strokeWidth = width
269 - ..strokeCap = StrokeCap.round; 269 + ..strokeCap = StrokeCap.butt;
270 270
271 final fullLapPaint = Paint() 271 final fullLapPaint = Paint()
272 ..color = progressColor 272 ..color = progressColor
@@ -286,7 +286,14 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -286,7 +286,14 @@ class _ActivityBurnRingPainter extends CustomPainter {
286 ); 286 );
287 } 287 }
288 288
289 - final endAngle = startAngle + math.pi * 2 * visibleProgress; 289 + final endAngle = isExactLap
  290 + ? startAngle
  291 + : _endCapAngle(
  292 + startAngle: startAngle,
  293 + radius: radius,
  294 + width: width,
  295 + progress: visibleProgress,
  296 + );
290 297
291 // Head layer: its shadow is painted first, then the colored current lap. 298 // Head layer: its shadow is painted first, then the colored current lap.
292 // For an exact lap there is no partial arc, so paint a colored head at the 299 // For an exact lap there is no partial arc, so paint a colored head at the
@@ -309,16 +316,71 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -309,16 +316,71 @@ class _ActivityBurnRingPainter extends CustomPainter {
309 color: progressColor, 316 color: progressColor,
310 ); 317 );
311 } else { 318 } else {
312 - canvas.drawArc(  
313 - rect,  
314 - startAngle,  
315 - math.pi * 2 * visibleProgress,  
316 - false,  
317 - progressPaint, 319 + _drawPartialRingWithRoundedCaps(
  320 + canvas,
  321 + rect: rect,
  322 + startAngle: startAngle,
  323 + radius: radius,
  324 + progress: visibleProgress,
  325 + paint: progressPaint,
318 ); 326 );
319 } 327 }
320 } 328 }
321 329
  330 + /// Matches the home-page ring: shrink the butt-capped arc, then add two
  331 + /// circular caps so that rounded ends never consume the remaining gap.
  332 + void _drawPartialRingWithRoundedCaps(
  333 + Canvas canvas, {
  334 + required Rect rect,
  335 + required double startAngle,
  336 + required double radius,
  337 + required double progress,
  338 + required Paint paint,
  339 + }) {
  340 + final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
  341 + if (sweep <= 0) return;
  342 +
  343 + final capAngle = math.asin((paint.strokeWidth / 2) / radius);
  344 + final actualCapAngle = math.min(capAngle, sweep / 2).toDouble();
  345 + final arcStart = startAngle + actualCapAngle;
  346 + final arcSweep = sweep - actualCapAngle * 2;
  347 +
  348 + if (arcSweep > 0) {
  349 + canvas.drawArc(rect, arcStart, arcSweep, false, paint);
  350 + }
  351 +
  352 + final capPaint = Paint()
  353 + ..color = paint.color
  354 + ..style = PaintingStyle.fill;
  355 + final center = rect.center;
  356 + canvas.drawCircle(
  357 + _pointOnRing(center, radius, arcStart),
  358 + paint.strokeWidth / 2,
  359 + capPaint,
  360 + );
  361 + canvas.drawCircle(
  362 + _pointOnRing(center, radius, arcStart + arcSweep),
  363 + paint.strokeWidth / 2,
  364 + capPaint,
  365 + );
  366 + }
  367 +
  368 + double _endCapAngle({
  369 + required double startAngle,
  370 + required double radius,
  371 + required double width,
  372 + required double progress,
  373 + }) {
  374 + final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
  375 + final capAngle = math.asin((width / 2) / radius);
  376 + return startAngle + sweep - math.min(capAngle, sweep / 2).toDouble();
  377 + }
  378 +
  379 + Offset _pointOnRing(Offset center, double radius, double angle) => Offset(
  380 + center.dx + math.cos(angle) * radius,
  381 + center.dy + math.sin(angle) * radius,
  382 + );
  383 +
322 void _drawRingEndCap( 384 void _drawRingEndCap(
323 Canvas canvas, { 385 Canvas canvas, {
324 required Offset center, 386 required Offset center,
@@ -27,6 +27,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { @@ -27,6 +27,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
27 static const _weekDateRangeType = 0; 27 static const _weekDateRangeType = 0;
28 static const _monthDateRangeType = 1; 28 static const _monthDateRangeType = 1;
29 static const _dayDateRangeType = 3; 29 static const _dayDateRangeType = 3;
  30 + static const _defaultSleepTargetDurationSeconds = 8 * 60 * 60;
30 31
31 final HealthDataSource _healthDataSource; 32 final HealthDataSource _healthDataSource;
32 33
@@ -95,7 +96,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { @@ -95,7 +96,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
95 SleepReport.empty(start.add(Duration(days: i))), 96 SleepReport.empty(start.add(Duration(days: i))),
96 ], 97 ],
97 averageDurationSeconds: data.avgSleepDuration, 98 averageDurationSeconds: data.avgSleepDuration,
98 - targetDurationSeconds: data.sleepTargetDuration, 99 + targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
99 averageScore: data.avgSleepScore, 100 averageScore: data.avgSleepScore,
100 averageSleepEvaluate: data.avgSleepEvaluate?.toInt(), 101 averageSleepEvaluate: data.avgSleepEvaluate?.toInt(),
101 previousAverageDurationSeconds: data.qoqAvgSleepDuration, 102 previousAverageDurationSeconds: data.qoqAvgSleepDuration,
@@ -140,7 +141,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { @@ -140,7 +141,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
140 SleepReport.empty(start.add(Duration(days: i))), 141 SleepReport.empty(start.add(Duration(days: i))),
141 ], 142 ],
142 averageDurationSeconds: data.avgSleepDuration, 143 averageDurationSeconds: data.avgSleepDuration,
143 - targetDurationSeconds: data.sleepTargetDuration, 144 + targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
144 averageScore: data.avgSleepScore, 145 averageScore: data.avgSleepScore,
145 averageSleepEvaluate: data.avgSleepEvaluate?.toInt(), 146 averageSleepEvaluate: data.avgSleepEvaluate?.toInt(),
146 previousAverageDurationSeconds: data.qoqAvgSleepDuration, 147 previousAverageDurationSeconds: data.qoqAvgSleepDuration,
@@ -280,7 +281,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource { @@ -280,7 +281,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
280 asleepTime, 281 asleepTime,
281 points, 282 points,
282 sleepDurationSeconds: dailyTrend.totalTime, 283 sleepDurationSeconds: dailyTrend.totalTime,
283 - targetDurationSeconds: data.sleepTargetDuration, 284 + targetDurationSeconds: _targetDurationSeconds(data.sleepTargetDuration),
284 averageBpm: data.avgHr?.toInt(), 285 averageBpm: data.avgHr?.toInt(),
285 maxBpm: data.maxHr?.toInt(), 286 maxBpm: data.maxHr?.toInt(),
286 minBpm: data.minHr?.toInt(), 287 minBpm: data.minHr?.toInt(),
@@ -378,6 +379,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource { @@ -378,6 +379,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
378 ); 379 );
379 } 380 }
380 381
  382 + num _targetDurationSeconds(num? value) =>
  383 + value == null || value <= 0 ? _defaultSleepTargetDurationSeconds : value;
  384 +
381 SleepHeartRateSummary _heartRateSummary( 385 SleepHeartRateSummary _heartRateSummary(
382 List<SleepHeartRatePoint> points, 386 List<SleepHeartRatePoint> points,
383 DateTime? sleepStart, 387 DateTime? sleepStart,
@@ -157,33 +157,81 @@ class _SleepQualityRingPainter extends CustomPainter { @@ -157,33 +157,81 @@ class _SleepQualityRingPainter extends CustomPainter {
157 ..color = _purple 157 ..color = _purple
158 ..style = PaintingStyle.stroke 158 ..style = PaintingStyle.stroke
159 ..strokeWidth = 22 159 ..strokeWidth = 22
160 - ..strokeCap = StrokeCap.round; 160 + ..strokeCap = StrokeCap.butt;
161 final innerProgress = Paint() 161 final innerProgress = Paint()
162 ..color = _blue 162 ..color = _blue
163 ..style = PaintingStyle.stroke 163 ..style = PaintingStyle.stroke
164 ..strokeWidth = 22 164 ..strokeWidth = 22
165 - ..strokeCap = StrokeCap.round; 165 + ..strokeCap = StrokeCap.butt;
166 166
167 if (hasDurationTarget) { 167 if (hasDurationTarget) {
168 - canvas.drawArc(  
169 - outerRect,  
170 - startAngle,  
171 - math.pi * 2 * durationProgress,  
172 - false,  
173 - outerProgress, 168 + _drawProgressArc(
  169 + canvas,
  170 + rect: outerRect,
  171 + startAngle: startAngle,
  172 + progress: durationProgress,
  173 + paint: outerProgress,
174 ); 174 );
175 } 175 }
176 if (hasQuality) { 176 if (hasQuality) {
177 - canvas.drawArc(  
178 - innerRect,  
179 - startAngle,  
180 - math.pi * 2 * qualityProgress,  
181 - false,  
182 - innerProgress, 177 + _drawProgressArc(
  178 + canvas,
  179 + rect: innerRect,
  180 + startAngle: startAngle,
  181 + progress: qualityProgress,
  182 + paint: innerProgress,
183 ); 183 );
184 } 184 }
185 } 185 }
186 186
  187 + /// Draws rounded ends without allowing them to consume the remaining gap.
  188 + /// This preserves the visual progress ratio for near-complete circles.
  189 + void _drawProgressArc(
  190 + Canvas canvas, {
  191 + required Rect rect,
  192 + required double startAngle,
  193 + required double progress,
  194 + required Paint paint,
  195 + }) {
  196 + final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble();
  197 + if (sweep <= 0) return;
  198 +
  199 + if (sweep >= math.pi * 2) {
  200 + canvas.drawArc(rect, startAngle, math.pi * 2, false, paint);
  201 + return;
  202 + }
  203 +
  204 + final radius = rect.width / 2;
  205 + final capAngle = math.asin((paint.strokeWidth / 2) / radius);
  206 + final actualCapAngle = math.min(capAngle, sweep / 2).toDouble();
  207 + final arcStart = startAngle + actualCapAngle;
  208 + final arcSweep = sweep - actualCapAngle * 2;
  209 + final center = rect.center;
  210 +
  211 + if (arcSweep > 0) {
  212 + canvas.drawArc(rect, arcStart, arcSweep, false, paint);
  213 + }
  214 +
  215 + final capPaint = Paint()
  216 + ..color = paint.color
  217 + ..style = PaintingStyle.fill;
  218 + canvas.drawCircle(
  219 + _pointOnCircle(center, radius, arcStart),
  220 + paint.strokeWidth / 2,
  221 + capPaint,
  222 + );
  223 + canvas.drawCircle(
  224 + _pointOnCircle(center, radius, arcStart + arcSweep),
  225 + paint.strokeWidth / 2,
  226 + capPaint,
  227 + );
  228 + }
  229 +
  230 + Offset _pointOnCircle(Offset center, double radius, double angle) => Offset(
  231 + center.dx + radius * math.cos(angle),
  232 + center.dy + radius * math.sin(angle),
  233 + );
  234 +
187 @override 235 @override
188 bool shouldRepaint(covariant _SleepQualityRingPainter oldDelegate) { 236 bool shouldRepaint(covariant _SleepQualityRingPainter oldDelegate) {
189 return oldDelegate.durationProgress != durationProgress || 237 return oldDelegate.durationProgress != durationProgress ||