mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

PR #147801 introduced some convenient `AnimationStatus` getters, but I just realized that `AnimationController` now has 2 getters for the same thing: `isAnimating` and `isRunning`. The intent of this pull request is to correct that mistake, and implement the getters in the appropriate places.
211 lines
6.3 KiB
Dart
211 lines
6.3 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());
|
|
}
|