From 78ccced805c02dd655de753727e15c171c568bb3 Mon Sep 17 00:00:00 2001 From: Andrew Wilson Date: Fri, 18 Jun 2021 17:14:02 -0700 Subject: [PATCH] Fix leak of CurvedAnimations in long-lived ImplicitlyAnimatedWidgets. (#84785) --- .../flutter/lib/src/animation/animations.dart | 9 ++++ .../lib/src/widgets/implicit_animations.dart | 5 +- .../test/animation/animations_test.dart | 38 +++++++++++++ .../widgets/implicit_animations_test.dart | 53 +++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/animation/animations.dart b/packages/flutter/lib/src/animation/animations.dart index 70f8171e75a..1af955ba055 100644 --- a/packages/flutter/lib/src/animation/animations.dart +++ b/packages/flutter/lib/src/animation/animations.dart @@ -408,6 +408,9 @@ class CurvedAnimation extends Animation with AnimationWithParentMixin with AnimationWithParentMixin @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? tween, dynamic targetValue, TweenConstructor constructor) { @@ -401,6 +403,7 @@ abstract class ImplicitlyAnimatedWidgetState @override void dispose() { + (_animation as CurvedAnimation).dispose(); _controller.dispose(); super.dispose(); } diff --git a/packages/flutter/test/animation/animations_test.dart b/packages/flutter/test/animation/animations_test.dart index 77f149d84c6..575d4eac019 100644 --- a/packages/flutter/test/animation/animations_test.dart +++ b/packages/flutter/test/animation/animations_test.dart @@ -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), diff --git a/packages/flutter/test/widgets/implicit_animations_test.dart b/packages/flutter/test/widgets/implicit_animations_test.dart index b9a356ce405..163ff2c6af9 100644 --- a/packages/flutter/test/widgets/implicit_animations_test.dart +++ b/packages/flutter/test/widgets/implicit_animations_test.dart @@ -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> key = + GlobalKey>(); + final ValueNotifier curve = ValueNotifier(const Interval(0.0, 0.5)); + await tester.pumpWidget(wrap( + child: ValueListenableBuilder( + 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? firstState = key.currentState; + final Animation? 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? secondState = key.currentState; + final Animation? 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}) {