diet-guard/app/lib/widgets/progress_motion.dart
Krzysztof kuhy Rudnicki 97af520fdd
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Add log sound cue and progress card motion
- lightImpact + 160 ms confirm cue on a validated log (audioplayers,
  no audio focus); device-local opt-out switch in Settings, on by default
- post-log card: confirmation line (slot + the meal's kcal), entrance and
  kcal/streak count-up on motion tokens transcribed into ui/theme.dart;
  durations collapse to zero under disableAnimations
- test/flutter_test_config.dart mutes the cue suite-wide

Todo: 8e66c1b9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWVHMLzeojNP4AaPoEj
2026-11-02 21:12:46 +01:00

67 lines
2.0 KiB
Dart

/// Motion for the post-log progress card: an entrance and a count-up.
library;
import 'package:diet_guard_app/ui/theme.dart';
import 'package:flutter/material.dart';
/// Fades [child] in and slides it up by [AppSpacing.md] when first built.
///
/// Key it per card instance to replay it; under reduced motion the first
/// frame is already the final one.
class ProgressEntrance extends StatelessWidget {
/// Creates a [ProgressEntrance].
const ProgressEntrance({required this.child, super.key});
/// The widget to bring in.
final Widget child;
@override
Widget build(BuildContext context) => TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: AppDuration.resolve(context, AppDuration.base),
curve: AppCurve.decelerate,
child: child,
builder: (context, t, child) => Opacity(
opacity: t,
child: Transform.translate(
offset: Offset(0, (1 - t) * AppSpacing.md),
child: child,
),
),
);
}
/// Drives [builder] with a number running from [from] to [to].
///
/// Neutral on purpose: it never knows whether the number is "good", so no
/// value gets a different curve or colour for being under budget. The last
/// frame passes [to] untouched, so callers render exactly the static value.
class CountUp extends StatelessWidget {
/// Creates a [CountUp].
const CountUp({
required this.from,
required this.to,
required this.builder,
super.key,
});
/// Starting value.
final double from;
/// Final value.
final double to;
/// Builds the UI for the current animated value.
final Widget Function(BuildContext context, double value) builder;
@override
Widget build(BuildContext context) => TweenAnimationBuilder<double>(
tween: Tween<double>(begin: from, end: to),
duration: from == to
? Duration.zero
: AppDuration.resolve(context, AppDuration.slow),
curve: AppCurve.standard,
builder: (context, value, _) => builder(context, value),
);
}