diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart index c74b6105bec..a2ba25aafb7 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -82,6 +82,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { super.textDirection, super.child, Clip clipBehavior = Clip.hardEdge, + VoidCallback? onEnd, }) : _vsync = vsync, _clipBehavior = clipBehavior { _controller = AnimationController( @@ -97,6 +98,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { parent: _controller, curve: curve, ); + _onEnd = onEnd; } /// When asserts are enabled, returns the animation controller that is used @@ -203,6 +205,19 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _controller.resync(vsync); } + /// Called every time an animation completes. + /// + /// This can be useful to trigger additional actions (e.g. another animation) + /// at the end of the current animation. + VoidCallback? get onEnd => _onEnd; + VoidCallback? _onEnd; + set onEnd(VoidCallback? value) { + if (value == _onEnd) { + return; + } + _onEnd = value; + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -216,11 +231,13 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { // already, to resume interrupted resizing animation. markNeedsLayout(); } + _controller.addStatusListener(_animationStatusListener); } @override void detach() { _controller.stop(); + _controller.removeStatusListener(_animationStatusListener); super.detach(); } @@ -363,6 +380,16 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { } } + void _animationStatusListener(AnimationStatus status) { + switch (status) { + case AnimationStatus.completed: + _onEnd?.call(); + case AnimationStatus.dismissed: + case AnimationStatus.forward: + case AnimationStatus.reverse: + } + } + @override void paint(PaintingContext context, Offset offset) { if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) { diff --git a/packages/flutter/lib/src/widgets/animated_size.dart b/packages/flutter/lib/src/widgets/animated_size.dart index 3deb0d76602..110ec3ca1af 100644 --- a/packages/flutter/lib/src/widgets/animated_size.dart +++ b/packages/flutter/lib/src/widgets/animated_size.dart @@ -31,6 +31,7 @@ class AnimatedSize extends StatefulWidget { required this.duration, this.reverseDuration, this.clipBehavior = Clip.hardEdge, + this.onEnd, }); /// The widget below this widget in the tree. @@ -78,6 +79,12 @@ class AnimatedSize extends StatefulWidget { /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; + /// Called every time an animation completes. + /// + /// This can be useful to trigger additional actions (e.g. another animation) + /// at the end of the current animation. + final VoidCallback? onEnd; + @override State createState() => _AnimatedSizeState(); } @@ -93,6 +100,7 @@ class _AnimatedSizeState reverseDuration: widget.reverseDuration, vsync: this, clipBehavior: widget.clipBehavior, + onEnd: widget.onEnd, child: widget.child, ); } @@ -107,6 +115,7 @@ class _AnimatedSize extends SingleChildRenderObjectWidget { this.reverseDuration, required this.vsync, this.clipBehavior = Clip.hardEdge, + this.onEnd, }); final AlignmentGeometry alignment; @@ -119,6 +128,8 @@ class _AnimatedSize extends SingleChildRenderObjectWidget { final Clip clipBehavior; + final VoidCallback? onEnd; + @override RenderAnimatedSize createRenderObject(BuildContext context) { return RenderAnimatedSize( @@ -129,6 +140,7 @@ class _AnimatedSize extends SingleChildRenderObjectWidget { vsync: vsync, textDirection: Directionality.maybeOf(context), clipBehavior: clipBehavior, + onEnd: onEnd, ); } @@ -141,7 +153,8 @@ class _AnimatedSize extends SingleChildRenderObjectWidget { ..curve = curve ..vsync = vsync ..textDirection = Directionality.maybeOf(context) - ..clipBehavior = clipBehavior; + ..clipBehavior = clipBehavior + ..onEnd = onEnd; } @override diff --git a/packages/flutter/test/widgets/animated_size_test.dart b/packages/flutter/test/widgets/animated_size_test.dart index d6b851475c1..54927edf3cf 100644 --- a/packages/flutter/test/widgets/animated_size_test.dart +++ b/packages/flutter/test/widgets/animated_size_test.dart @@ -87,6 +87,61 @@ void main() { expect(box.size.height, equals(100.0)); }); + testWidgets('calls onEnd when animation is completed', (WidgetTester tester) async { + int callCount = 0; + void handleEnd() { + callCount++; + } + + await tester.pumpWidget( + Center( + child: AnimatedSize( + onEnd: handleEnd, + duration: const Duration(milliseconds: 200), + child: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ), + ); + + expect(callCount, equals(0)); + + await tester.pumpWidget( + Center( + child: AnimatedSize( + onEnd: handleEnd, + duration: const Duration(milliseconds: 200), + child: const SizedBox( + width: 200.0, + height: 200.0, + ), + ), + ), + ); + + expect(callCount, equals(0)); + await tester.pumpAndSettle(); + expect(callCount, equals(1)); + + await tester.pumpWidget( + Center( + child: AnimatedSize( + onEnd: handleEnd, + duration: const Duration(milliseconds: 200), + child: const SizedBox( + width: 100.0, + height: 100.0, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(callCount, equals(2)); + }); + testWidgets('clamps animated size to constraints', (WidgetTester tester) async { await tester.pumpWidget( const Center(