Fix leak of CurvedAnimations in long-lived ImplicitlyAnimatedWidgets. (#84785)

This commit is contained in:
Andrew Wilson 2021-06-18 17:14:02 -07:00 committed by GitHub
parent 05736b55aa
commit 78ccced805
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 1 deletions

View file

@ -408,6 +408,9 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
/// animation is used to animate.
AnimationStatus? _curveDirection;
/// True if this CurvedAnimation has been disposed.
bool isDisposed = false;
void _updateCurveDirection(AnimationStatus status) {
switch (status) {
case AnimationStatus.dismissed:
@ -427,6 +430,12 @@ class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<do
return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse;
}
/// Cleans up any listeners added by this CurvedAnimation.
void dispose() {
isDisposed = true;
parent.removeStatusListener(_updateCurveDirection);
}
@override
double get value {
final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;

View file

@ -380,8 +380,10 @@ abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget>
@override
void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve)
if (widget.curve != oldWidget.curve) {
(_animation as CurvedAnimation).dispose();
_animation = _createCurve();
}
_controller.duration = widget.duration;
if (_constructTweens()) {
forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
@ -401,6 +403,7 @@ abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget>
@override
void dispose() {
(_animation as CurvedAnimation).dispose();
_controller.dispose();
super.dispose();
}

View file

@ -302,6 +302,44 @@ FlutterError
expect(curved.value, moreOrLessEquals(0.0));
});
test('CurvedAnimation stops listening to parent when disposed.', () async {
const Interval forwardCurve = Interval(0.0, 0.5);
const Interval reverseCurve = Interval(0.5, 1.0);
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
reverseDuration: const Duration(milliseconds: 100),
vsync: const TestVSync(),
);
final CurvedAnimation curved = CurvedAnimation(
parent: controller, curve: forwardCurve, reverseCurve: reverseCurve);
expect(forwardCurve.transform(0.5), 1.0);
expect(reverseCurve.transform(0.5), 0.0);
controller.forward(from: 0.5);
expect(controller.status, equals(AnimationStatus.forward));
expect(curved.value, equals(1.0));
controller.value = 1.0;
expect(controller.status, equals(AnimationStatus.completed));
controller.reverse(from: 0.5);
expect(controller.status, equals(AnimationStatus.reverse));
expect(curved.value, equals(0.0));
expect(curved.isDisposed, isFalse);
curved.dispose();
expect(curved.isDisposed, isTrue);
controller.value = 0.0;
expect(controller.status, equals(AnimationStatus.dismissed));
controller.forward(from: 0.5);
expect(controller.status, equals(AnimationStatus.forward));
expect(curved.value, equals(0.0));
});
test('ReverseAnimation running with different forward and reverse durations.', () {
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),

View file

@ -350,6 +350,59 @@ void main() {
await tester.pump(additionalDelay);
expect(mockOnEndFunction.called, 1);
});
testWidgets('Ensure CurvedAnimations are disposed on widget change',
(WidgetTester tester) async {
final GlobalKey<ImplicitlyAnimatedWidgetState<AnimatedOpacity>> key =
GlobalKey<ImplicitlyAnimatedWidgetState<AnimatedOpacity>>();
final ValueNotifier<Curve> curve = ValueNotifier<Curve>(const Interval(0.0, 0.5));
await tester.pumpWidget(wrap(
child: ValueListenableBuilder<Curve>(
valueListenable: curve,
builder: (_, Curve c, __) => AnimatedOpacity(
key: key,
opacity: 1.0,
duration: const Duration(seconds: 1),
curve: c,
child: Container(color: Colors.green)),
),
));
final ImplicitlyAnimatedWidgetState<AnimatedOpacity>? firstState = key.currentState;
final Animation<double>? firstAnimation = firstState?.animation;
if (firstAnimation == null)
fail('animation was null!');
final CurvedAnimation firstCurvedAnimation =
firstAnimation as CurvedAnimation;
expect(firstCurvedAnimation.isDisposed, isFalse);
curve.value = const Interval(0.0, 0.6);
await tester.pumpAndSettle();
final ImplicitlyAnimatedWidgetState<AnimatedOpacity>? secondState = key.currentState;
final Animation<double>? secondAnimation = secondState?.animation;
if (secondAnimation == null)
fail('animation was null!');
final CurvedAnimation secondCurvedAnimation = secondAnimation as CurvedAnimation;
expect(firstState, equals(secondState));
expect(firstAnimation, isNot(equals(secondAnimation)));
expect(firstCurvedAnimation.isDisposed, isTrue);
expect(secondCurvedAnimation.isDisposed, isFalse);
await tester.pumpWidget(
wrap(
child: const Offstage(),
),
);
await tester.pumpAndSettle();
expect(secondCurvedAnimation.isDisposed, isTrue);
});
}
Widget wrap({required Widget child}) {