mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
Fix leak of CurvedAnimations in long-lived ImplicitlyAnimatedWidgets. (#84785)
This commit is contained in:
parent
05736b55aa
commit
78ccced805
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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}) {
|
||||
|
|
Loading…
Reference in a new issue