Commit 8cf44b6bca41627770b80193f44827b27159ba40

Authored by 刘宏哲
1 parent 3f8522dc

feat(app): update ui

@@ -395,7 +395,7 @@ class _MonthGrid extends StatelessWidget { @@ -395,7 +395,7 @@ class _MonthGrid extends StatelessWidget {
395 dayBuilder: (context, day) => Column( 395 dayBuilder: (context, day) => Column(
396 mainAxisSize: MainAxisSize.min, 396 mainAxisSize: MainAxisSize.min,
397 children: [ 397 children: [
398 - ActivityBurnRing(report: day, size: 32), 398 + ActivityBurnRing(report: day, size: 32, animate: true),
399 const SizedBox(height: 4), 399 const SizedBox(height: 4),
400 Text( 400 Text(
401 day.date.day.toString(), 401 day.date.day.toString(),
@@ -51,11 +51,27 @@ class _ActivityBurnRingState extends State<ActivityBurnRing> @@ -51,11 +51,27 @@ class _ActivityBurnRingState extends State<ActivityBurnRing>
51 _animation = _RingProgressTween(begin: from, end: target).animate( 51 _animation = _RingProgressTween(begin: from, end: target).animate(
52 CurvedAnimation(parent: _controller, curve: Curves.easeInOutQuad), 52 CurvedAnimation(parent: _controller, curve: Curves.easeInOutQuad),
53 ); 53 );
54 - _controller.duration =  
55 - widget.animate ? const Duration(seconds: 1) : Duration.zero; 54 + _controller.duration = _animationDuration(from: from, target: target);
56 _controller.forward(from: 0); 55 _controller.forward(from: 0);
57 } 56 }
58 57
  58 + Duration _animationDuration({
  59 + required _RingProgress from,
  60 + required _RingProgress target,
  61 + }) {
  62 + if (!widget.animate) return Duration.zero;
  63 +
  64 + final visualDelta = _RingProgressTween.maximumVisualDelta(from, target);
  65 + if (visualDelta == 0) return Duration.zero;
  66 +
  67 + // A tiny drawable change should not spend a full second creeping forward.
  68 + final milliseconds = (1000 * visualDelta.clamp(0, 1))
  69 + .round()
  70 + .clamp(150, 1000)
  71 + .toInt();
  72 + return Duration(milliseconds: milliseconds);
  73 + }
  74 +
59 @override 75 @override
60 void dispose() { 76 void dispose() {
61 _controller.dispose(); 77 _controller.dispose();
@@ -116,12 +132,101 @@ class _RingProgress { @@ -116,12 +132,101 @@ class _RingProgress {
116 class _RingProgressTween extends Tween<_RingProgress> { 132 class _RingProgressTween extends Tween<_RingProgress> {
117 _RingProgressTween({required super.begin, required super.end}); 133 _RingProgressTween({required super.begin, required super.end});
118 134
  135 + static final _activeMinimumProgress = _minimumProgress(
  136 + radius: _ActivityBurnRingPainter._outerRadius,
  137 + width: _ActivityBurnRingPainter._ringWidth,
  138 + );
  139 + static final _exerciseMinimumProgress = _minimumProgress(
  140 + radius: _ActivityBurnRingPainter._middleRadius,
  141 + width: _ActivityBurnRingPainter._ringWidth,
  142 + );
  143 + static final _standMinimumProgress = _minimumProgress(
  144 + radius: _ActivityBurnRingPainter._innerRadius,
  145 + width: _ActivityBurnRingPainter._ringWidth,
  146 + );
  147 +
119 @override 148 @override
120 _RingProgress lerp(double t) => _RingProgress( 149 _RingProgress lerp(double t) => _RingProgress(
121 - active: begin!.active + (end!.active - begin!.active) * t,  
122 - exercise: begin!.exercise + (end!.exercise - begin!.exercise) * t,  
123 - stand: begin!.stand + (end!.stand - begin!.stand) * t, 150 + active: _lerpProgress(
  151 + begin!.active,
  152 + end!.active,
  153 + t,
  154 + minimumProgress: _activeMinimumProgress,
  155 + ),
  156 + exercise: _lerpProgress(
  157 + begin!.exercise,
  158 + end!.exercise,
  159 + t,
  160 + minimumProgress: _exerciseMinimumProgress,
  161 + ),
  162 + stand: _lerpProgress(
  163 + begin!.stand,
  164 + end!.stand,
  165 + t,
  166 + minimumProgress: _standMinimumProgress,
  167 + ),
  168 + );
  169 +
  170 + static double _minimumProgress({
  171 + required double radius,
  172 + required double width,
  173 + }) =>
  174 + math.asin((width / 2) / radius) / math.pi;
  175 +
  176 + static double maximumVisualDelta(
  177 + _RingProgress from,
  178 + _RingProgress target,
  179 + ) =>
  180 + math.max(
  181 + _visualDelta(
  182 + from.active,
  183 + target.active,
  184 + _activeMinimumProgress,
  185 + ),
  186 + math.max(
  187 + _visualDelta(
  188 + from.exercise,
  189 + target.exercise,
  190 + _exerciseMinimumProgress,
  191 + ),
  192 + _visualDelta(
  193 + from.stand,
  194 + target.stand,
  195 + _standMinimumProgress,
  196 + ),
  197 + ),
124 ); 198 );
  199 +
  200 + static double _visualDelta(
  201 + double begin,
  202 + double end,
  203 + double minimumProgress,
  204 + ) {
  205 + if (begin == 0 && end > 0) {
  206 + return end - math.min(end, minimumProgress);
  207 + }
  208 + return (_visibleProgress(begin, minimumProgress) -
  209 + _visibleProgress(end, minimumProgress))
  210 + .abs();
  211 + }
  212 +
  213 + static double _visibleProgress(double value, double minimumProgress) {
  214 + if (value <= 0 || value >= 1) return value;
  215 + return math.max(value, minimumProgress).toDouble();
  216 + }
  217 +
  218 + double _lerpProgress(
  219 + double begin,
  220 + double end,
  221 + double t, {
  222 + required double minimumProgress,
  223 + }) {
  224 + if (begin == 0 && end > 0) {
  225 + final visualStart = math.min(end, minimumProgress);
  226 + return visualStart + (end - visualStart) * t;
  227 + }
  228 + return begin + (end - begin) * t;
  229 + }
125 } 230 }
126 231
127 class _ActivityBurnRingPainter extends CustomPainter { 232 class _ActivityBurnRingPainter extends CustomPainter {
@@ -163,7 +268,6 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -163,7 +268,6 @@ class _ActivityBurnRingPainter extends CustomPainter {
163 center: center, 268 center: center,
164 radius: _outerRadius * scale, 269 radius: _outerRadius * scale,
165 width: _ringWidth * scale, 270 width: _ringWidth * scale,
166 - trackColor: _track,  
167 progressColor: _active, 271 progressColor: _active,
168 progress: progress.active, 272 progress: progress.active,
169 startAngle: startAngle, 273 startAngle: startAngle,
@@ -173,7 +277,6 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -173,7 +277,6 @@ class _ActivityBurnRingPainter extends CustomPainter {
173 center: center, 277 center: center,
174 radius: _middleRadius * scale, 278 radius: _middleRadius * scale,
175 width: _ringWidth * scale, 279 width: _ringWidth * scale,
176 - trackColor: _innerTrack,  
177 progressColor: _exercise, 280 progressColor: _exercise,
178 progress: progress.exercise, 281 progress: progress.exercise,
179 startAngle: startAngle, 282 startAngle: startAngle,
@@ -183,7 +286,6 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -183,7 +286,6 @@ class _ActivityBurnRingPainter extends CustomPainter {
183 center: center, 286 center: center,
184 radius: _innerRadius * scale, 287 radius: _innerRadius * scale,
185 width: _ringWidth * scale, 288 width: _ringWidth * scale,
186 - trackColor: _center,  
187 progressColor: _stand, 289 progressColor: _stand,
188 progress: progress.stand, 290 progress: progress.stand,
189 startAngle: startAngle, 291 startAngle: startAngle,
@@ -248,7 +350,6 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -248,7 +350,6 @@ class _ActivityBurnRingPainter extends CustomPainter {
248 required Offset center, 350 required Offset center,
249 required double radius, 351 required double radius,
250 required double width, 352 required double width,
251 - required Color trackColor,  
252 required Color progressColor, 353 required Color progressColor,
253 required double progress, 354 required double progress,
254 required double startAngle, 355 required double startAngle,
@@ -329,6 +430,10 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -329,6 +430,10 @@ class _ActivityBurnRingPainter extends CustomPainter {
329 430
330 /// Matches the home-page ring: shrink the butt-capped arc, then add two 431 /// 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. 432 /// circular caps so that rounded ends never consume the remaining gap.
  433 + ///
  434 + /// A non-zero arc shorter than the diameter of its caps is rendered at the
  435 + /// minimum visible length. This avoids the merged cap's center drifting
  436 + /// clockwise while the value is animated from zero.
332 void _drawPartialRingWithRoundedCaps( 437 void _drawPartialRingWithRoundedCaps(
333 Canvas canvas, { 438 Canvas canvas, {
334 required Rect rect, 439 required Rect rect,
@@ -337,7 +442,12 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -337,7 +442,12 @@ class _ActivityBurnRingPainter extends CustomPainter {
337 required double progress, 442 required double progress,
338 required Paint paint, 443 required Paint paint,
339 }) { 444 }) {
340 - final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble(); 445 + final visibleProgress = _minimumVisibleProgress(
  446 + progress: progress,
  447 + radius: radius,
  448 + width: paint.strokeWidth,
  449 + );
  450 + final sweep = math.pi * 2 * visibleProgress;
341 if (sweep <= 0) return; 451 if (sweep <= 0) return;
342 452
343 final capAngle = math.asin((paint.strokeWidth / 2) / radius); 453 final capAngle = math.asin((paint.strokeWidth / 2) / radius);
@@ -371,11 +481,29 @@ class _ActivityBurnRingPainter extends CustomPainter { @@ -371,11 +481,29 @@ class _ActivityBurnRingPainter extends CustomPainter {
371 required double width, 481 required double width,
372 required double progress, 482 required double progress,
373 }) { 483 }) {
374 - final sweep = (math.pi * 2 * progress.clamp(0, 1)).toDouble(); 484 + final visibleProgress = _minimumVisibleProgress(
  485 + progress: progress,
  486 + radius: radius,
  487 + width: width,
  488 + );
  489 + final sweep = math.pi * 2 * visibleProgress;
375 final capAngle = math.asin((width / 2) / radius); 490 final capAngle = math.asin((width / 2) / radius);
376 return startAngle + sweep - math.min(capAngle, sweep / 2).toDouble(); 491 return startAngle + sweep - math.min(capAngle, sweep / 2).toDouble();
377 } 492 }
378 493
  494 + double _minimumVisibleProgress({
  495 + required double progress,
  496 + required double radius,
  497 + required double width,
  498 + }) {
  499 + final clampedProgress = progress.clamp(0, 1).toDouble();
  500 + if (clampedProgress <= 0) return 0;
  501 +
  502 + final capAngle = math.asin((width / 2) / radius);
  503 + final minimumProgress = capAngle / math.pi;
  504 + return math.max(clampedProgress, minimumProgress).toDouble();
  505 + }
  506 +
379 Offset _pointOnRing(Offset center, double radius, double angle) => Offset( 507 Offset _pointOnRing(Offset center, double radius, double angle) => Offset(
380 center.dx + math.cos(angle) * radius, 508 center.dx + math.cos(angle) * radius,
381 center.dy + math.sin(angle) * radius, 509 center.dy + math.sin(angle) * radius,
@@ -372,7 +372,7 @@ class _DayRing extends StatelessWidget { @@ -372,7 +372,7 @@ class _DayRing extends StatelessWidget {
372 ), 372 ),
373 ), 373 ),
374 const SizedBox(height: 4), 374 const SizedBox(height: 4),
375 - ActivityBurnRing(report: report, size: 32), 375 + ActivityBurnRing(report: report, size: 32, animate: true),
376 const SizedBox(height: 3), 376 const SizedBox(height: 3),
377 Text( 377 Text(
378 report.date.day.toString(), 378 report.date.day.toString(),