| // 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/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class TestPaintingContext implements PaintingContext { |
| final List<Invocation> invocations = <Invocation>[]; |
| |
| @override |
| void noSuchMethod(Invocation invocation) { |
| invocations.add(invocation); |
| } |
| } |
| |
| void main() { |
| group('AnimatedSize', () { |
| testWidgets('animates forwards then backwards with stable-sized children', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| RenderBox box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(100.0)); |
| expect(box.size.height, equals(100.0)); |
| |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 200.0, |
| height: 200.0, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(150.0)); |
| expect(box.size.height, equals(150.0)); |
| |
| TestPaintingContext context = TestPaintingContext(); |
| box.paint(context, Offset.zero); |
| expect(context.invocations.first.memberName, equals(#pushClipRect)); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(200.0)); |
| expect(box.size.height, equals(200.0)); |
| |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(150.0)); |
| expect(box.size.height, equals(150.0)); |
| |
| context = TestPaintingContext(); |
| box.paint(context, Offset.zero); |
| expect(context.invocations.first.memberName, equals(#paintChild)); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(100.0)); |
| expect(box.size.height, equals(100.0)); |
| }); |
| |
| testWidgets('clamps animated size to constraints', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Center( |
| child: SizedBox ( |
| width: 100.0, |
| height: 100.0, |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| RenderBox box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(100.0)); |
| expect(box.size.height, equals(100.0)); |
| |
| // Attempt to animate beyond the outer SizedBox. |
| await tester.pumpWidget( |
| const Center( |
| child: SizedBox ( |
| width: 100.0, |
| height: 100.0, |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 200.0, |
| height: 200.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Verify that animated size is the same as the outer SizedBox. |
| await tester.pump(const Duration(milliseconds: 100)); |
| box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(100.0)); |
| expect(box.size.height, equals(100.0)); |
| }); |
| |
| testWidgets('tracks unstable child, then resumes animation when child stabilizes', (WidgetTester tester) async { |
| Future<void> pumpMillis(int millis) async { |
| await tester.pump(Duration(milliseconds: millis)); |
| } |
| |
| void verify({ double? size, RenderAnimatedSizeState? state }) { |
| assert(size != null || state != null); |
| final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize)); |
| if (size != null) { |
| expect(box.size.width, size); |
| expect(box.size.height, size); |
| } |
| if (state != null) { |
| expect(box.state, state); |
| } |
| } |
| |
| await tester.pumpWidget( |
| Center( |
| child: AnimatedSize( |
| duration: const Duration(milliseconds: 200), |
| child: AnimatedContainer( |
| duration: const Duration(milliseconds: 100), |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| verify(size: 100.0, state: RenderAnimatedSizeState.stable); |
| |
| // Animate child size from 100 to 200 slowly (100ms). |
| await tester.pumpWidget( |
| Center( |
| child: AnimatedSize( |
| duration: const Duration(milliseconds: 200), |
| child: AnimatedContainer( |
| duration: const Duration(milliseconds: 100), |
| width: 200.0, |
| height: 200.0, |
| ), |
| ), |
| ), |
| ); |
| |
| // Make sure animation proceeds at child's pace, with AnimatedSize |
| // tightly tracking the child's size. |
| verify(state: RenderAnimatedSizeState.stable); |
| await pumpMillis(1); // register change |
| verify(state: RenderAnimatedSizeState.changed); |
| await pumpMillis(49); |
| verify(size: 150.0, state: RenderAnimatedSizeState.unstable); |
| await pumpMillis(50); |
| verify(size: 200.0, state: RenderAnimatedSizeState.unstable); |
| |
| // Stabilize size |
| await pumpMillis(50); |
| verify(size: 200.0, state: RenderAnimatedSizeState.stable); |
| |
| // Quickly (in 1ms) change size back to 100 |
| await tester.pumpWidget( |
| Center( |
| child: AnimatedSize( |
| duration: const Duration(milliseconds: 200), |
| child: AnimatedContainer( |
| duration: const Duration(milliseconds: 1), |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| verify(size: 200.0, state: RenderAnimatedSizeState.stable); |
| await pumpMillis(1); // register change |
| verify(state: RenderAnimatedSizeState.changed); |
| await pumpMillis(100); |
| verify(size: 150.0, state: RenderAnimatedSizeState.stable); |
| await pumpMillis(100); |
| verify(size: 100.0, state: RenderAnimatedSizeState.stable); |
| }); |
| |
| testWidgets('resyncs its animation controller', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 200.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| final RenderBox box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, equals(150.0)); |
| }); |
| |
| testWidgets('does not run animation unnecessarily', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| for (int i = 0; i < 20; i++) { |
| final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize)); |
| expect(box.size.width, 100.0); |
| expect(box.size.height, 100.0); |
| expect(box.state, RenderAnimatedSizeState.stable); |
| expect(box.isAnimating, false); |
| await tester.pump(const Duration(milliseconds: 10)); |
| } |
| }); |
| |
| testWidgets('can set and update clipBehavior', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Center( |
| child: AnimatedSize( |
| duration: Duration(milliseconds: 200), |
| child: SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| |
| // By default, clipBehavior should be Clip.hardEdge |
| final RenderAnimatedSize renderObject = tester.renderObject(find.byType(AnimatedSize)); |
| expect(renderObject.clipBehavior, equals(Clip.hardEdge)); |
| |
| for(final Clip clip in Clip.values) { |
| await tester.pumpWidget( |
| Center( |
| child: AnimatedSize( |
| duration: const Duration(milliseconds: 200), |
| clipBehavior: clip, |
| child: const SizedBox( |
| width: 100.0, |
| height: 100.0, |
| ), |
| ), |
| ), |
| ); |
| expect(renderObject.clipBehavior, clip); |
| } |
| }); |
| |
| testWidgets('works wrapped in IntrinsicHeight and Wrap', (WidgetTester tester) async { |
| Future<void> pumpWidget(Size size, [Duration? duration]) async { |
| return tester.pumpWidget( |
| Center( |
| child: IntrinsicHeight( |
| child: Wrap( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| AnimatedSize( |
| duration: const Duration(milliseconds: 200), |
| curve: Curves.easeInOutBack, |
| child: SizedBox( |
| width: size.width, |
| height: size.height, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| duration, |
| ); |
| } |
| |
| await pumpWidget(const Size(100, 100)); |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(100, 100)); |
| |
| await pumpWidget(const Size(150, 200)); |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(100, 100)); |
| |
| // Each pump triggers verification of dry layout. |
| for (int total = 0; total < 200; total += 10) { |
| await tester.pump(const Duration(milliseconds: 10)); |
| } |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(150, 200)); |
| |
| // Change every pump |
| await pumpWidget(const Size(100, 100)); |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(150, 200)); |
| |
| await pumpWidget(const Size(111, 111), const Duration(milliseconds: 10)); |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(111, 111)); |
| |
| await pumpWidget(const Size(222, 222), const Duration(milliseconds: 10)); |
| expect(tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size, const Size(222, 222)); |
| }); |
| |
| testWidgets('re-attach with interrupted animation', (WidgetTester tester) async { |
| const Key key1 = ValueKey<String>('key1'); |
| const Key key2 = ValueKey<String>('key2'); |
| late StateSetter setState; |
| Size childSize = const Size.square(100); |
| final Widget animatedSize = Center( |
| key: GlobalKey(debugLabel: 'animated size'), |
| // This SizedBox creates a relayout boundary so _cleanRelayoutBoundary |
| // does not mark the descendant render objects below the relayout boundary |
| // dirty. |
| child: SizedBox.fromSize( |
| size: const Size.square(200), |
| child: Center( |
| child: AnimatedSize( |
| duration: const Duration(seconds: 1), |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter stateSetter) { |
| setState = stateSetter; |
| return SizedBox.fromSize(size: childSize); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Row( |
| children: <Widget>[ |
| SizedBox( |
| key: key1, |
| height: 200, |
| child: animatedSize, |
| ), |
| const SizedBox( |
| key: key2, |
| height: 200, |
| ), |
| ], |
| ), |
| ) |
| ); |
| |
| setState(() { |
| childSize = const Size.square(150); |
| }); |
| // Kick off the resizing animation. |
| await tester.pump(); |
| |
| // Immediately reparent the AnimatedSize subtree to a different parent |
| // with the same incoming constraints. |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Row( |
| children: <Widget>[ |
| const SizedBox( |
| key: key1, |
| height: 200, |
| ), |
| SizedBox( |
| key: key2, |
| height: 200, |
| child: animatedSize, |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.renderObject<RenderBox>(find.byType(AnimatedSize)).size, |
| const Size.square(100), |
| ); |
| await tester.pumpAndSettle(); |
| // The animatedSize widget animates to the right size. |
| expect( |
| tester.renderObject<RenderBox>(find.byType(AnimatedSize)).size, |
| const Size.square(150), |
| ); |
| }); |
| }); |
| } |