flutter/examples/api/lib/animation/animation_controller/animated_digit.0.dart
Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

193 lines
6.1 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// An example of [AnimationController] and [SlideTransition].
// Occupies the same width as the widest single digit used by AnimatedDigit.
//
// By stacking this widget behind AnimatedDigit's visible digit, we
// ensure that AnimatedWidget's width will not change when its value
// changes. Typically digits like '8' or '9' are wider than '1'. If
// an app arranges several AnimatedDigits in a centered Row, we don't
// want the Row to wiggle when the digits change because the overall
// width of the Row changes.
class _PlaceholderDigit extends StatelessWidget {
const _PlaceholderDigit();
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(
context,
).textTheme.displayLarge!.copyWith(fontWeight: FontWeight.w500);
final Iterable<Widget> placeholderDigits = <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map<Widget>((
int n,
) {
return Text('$n', style: textStyle);
});
return Opacity(opacity: 0, child: Stack(children: placeholderDigits.toList()));
}
}
// Displays a single digit [value].
//
// When the value changes the old value slides upwards and out of sight
// at the same as the new value slides into view.
class AnimatedDigit extends StatefulWidget {
const AnimatedDigit({super.key, required this.value});
final int value;
@override
State<AnimatedDigit> createState() => _AnimatedDigitState();
}
class _AnimatedDigitState extends State<AnimatedDigit> with SingleTickerProviderStateMixin {
static const Duration defaultDuration = Duration(milliseconds: 300);
late final AnimationController controller;
late int incomingValue;
late int outgoingValue;
List<int> pendingValues =
<int>[]; // widget.value updates that occurred while the animation is underway
Duration duration = defaultDuration;
@override
void initState() {
super.initState();
controller = AnimationController(duration: duration, vsync: this);
controller.addStatusListener(handleAnimationCompleted);
incomingValue = widget.value;
outgoingValue = widget.value;
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void handleAnimationCompleted(AnimationStatus status) {
if (status.isCompleted) {
if (pendingValues.isNotEmpty) {
// Display the next pending value. The duration was scaled down
// in didUpdateWidget by the total number of pending values so
// that all of the pending changes are shown within
// defaultDuration of the last one (the past pending change).
controller.duration = duration;
animateValueUpdate(incomingValue, pendingValues.removeAt(0));
} else {
controller.duration = defaultDuration;
}
}
}
void animateValueUpdate(int outgoing, int incoming) {
setState(() {
outgoingValue = outgoing;
incomingValue = incoming;
controller.forward(from: 0);
});
}
// Rebuilding the widget with a new value causes the animations to run.
// If the widget is updated while the value is being changed the new
// value is added to pendingValues and is taken care of when the current
// animation is complete (see handleAnimationCompleted()).
@override
void didUpdateWidget(AnimatedDigit oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != oldWidget.value) {
if (controller.isAnimating) {
// We're in the middle of animating outgoingValue out and
// incomingValue in. Shorten the duration of the current
// animation as well as the duration for animations that
// will show the pending values.
pendingValues.add(widget.value);
final double percentRemaining = 1 - controller.value;
duration = defaultDuration * (1 / (percentRemaining + pendingValues.length));
controller.animateTo(1.0, duration: duration * percentRemaining);
} else {
animateValueUpdate(incomingValue, widget.value);
}
}
}
// When the controller runs forward both SlideTransitions' children
// animate upwards. This takes the outgoingValue out of sight and the
// incoming value into view. See animateValueUpdate().
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(context).textTheme.displayLarge!;
return ClipRect(
child: Stack(
children: <Widget>[
const _PlaceholderDigit(),
SlideTransition(
position: controller.drive(
Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, -1), // Out of view above the top.
),
),
child: Text(key: ValueKey<int>(outgoingValue), '$outgoingValue', style: textStyle),
),
SlideTransition(
position: controller.drive(
Tween<Offset>(
begin: const Offset(0, 1), // Out of view below the bottom.
end: Offset.zero,
),
),
child: Text(key: ValueKey<int>(incomingValue), '$incomingValue', style: textStyle),
),
],
),
);
}
}
class AnimatedDigitApp extends StatelessWidget {
const AnimatedDigitApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'AnimatedDigit', home: AnimatedDigitHome());
}
}
class AnimatedDigitHome extends StatefulWidget {
const AnimatedDigitHome({super.key});
@override
State<AnimatedDigitHome> createState() => _AnimatedDigitHomeState();
}
class _AnimatedDigitHomeState extends State<AnimatedDigitHome> {
int value = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: AnimatedDigit(value: value % 10)),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
value += 1;
});
},
tooltip: 'Increment Digit',
child: const Icon(Icons.add),
),
);
}
}
void main() {
runApp(const AnimatedDigitApp());
}