1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/rendering.dart';
6import 'package:flutter/widgets.dart';
7import 'package:flutter_test/flutter_test.dart';
8
9class TestPaintingContext implements PaintingContext {
10 final List<Invocation> invocations = <Invocation>[];
11
12 @override
13 void noSuchMethod(Invocation invocation) {
14 invocations.add(invocation);
15 }
16}
17
18void main() {
19 group('AnimatedSize', () {
20 testWidgets('animates forwards then backwards with stable-sized children', (
21 WidgetTester tester,
22 ) async {
23 await tester.pumpWidget(
24 const Center(
25 child: AnimatedSize(
26 duration: Duration(milliseconds: 200),
27 child: SizedBox(width: 100.0, height: 100.0),
28 ),
29 ),
30 );
31
32 RenderBox box = tester.renderObject(find.byType(AnimatedSize));
33 expect(box.size.width, equals(100.0));
34 expect(box.size.height, equals(100.0));
35
36 await tester.pumpWidget(
37 const Center(
38 child: AnimatedSize(
39 duration: Duration(milliseconds: 200),
40 child: SizedBox(width: 200.0, height: 200.0),
41 ),
42 ),
43 );
44
45 await tester.pump(const Duration(milliseconds: 100));
46 box = tester.renderObject(find.byType(AnimatedSize));
47 expect(box.size.width, equals(150.0));
48 expect(box.size.height, equals(150.0));
49
50 TestPaintingContext context = TestPaintingContext();
51 box.paint(context, Offset.zero);
52 expect(context.invocations.first.memberName, equals(#pushClipRect));
53
54 await tester.pump(const Duration(milliseconds: 100));
55 box = tester.renderObject(find.byType(AnimatedSize));
56 expect(box.size.width, equals(200.0));
57 expect(box.size.height, equals(200.0));
58
59 await tester.pumpWidget(
60 const Center(
61 child: AnimatedSize(
62 duration: Duration(milliseconds: 200),
63 child: SizedBox(width: 100.0, height: 100.0),
64 ),
65 ),
66 );
67
68 await tester.pump(const Duration(milliseconds: 100));
69 box = tester.renderObject(find.byType(AnimatedSize));
70 expect(box.size.width, equals(150.0));
71 expect(box.size.height, equals(150.0));
72
73 context = TestPaintingContext();
74 box.paint(context, Offset.zero);
75 expect(context.invocations.first.memberName, equals(#paintChild));
76
77 await tester.pump(const Duration(milliseconds: 100));
78 box = tester.renderObject(find.byType(AnimatedSize));
79 expect(box.size.width, equals(100.0));
80 expect(box.size.height, equals(100.0));
81 });
82
83 testWidgets('calls onEnd when animation is completed', (WidgetTester tester) async {
84 int callCount = 0;
85 void handleEnd() {
86 callCount++;
87 }
88
89 await tester.pumpWidget(
90 Center(
91 child: AnimatedSize(
92 onEnd: handleEnd,
93 duration: const Duration(milliseconds: 200),
94 child: const SizedBox(width: 100.0, height: 100.0),
95 ),
96 ),
97 );
98
99 expect(callCount, equals(0));
100
101 await tester.pumpWidget(
102 Center(
103 child: AnimatedSize(
104 onEnd: handleEnd,
105 duration: const Duration(milliseconds: 200),
106 child: const SizedBox(width: 200.0, height: 200.0),
107 ),
108 ),
109 );
110
111 expect(callCount, equals(0));
112 await tester.pumpAndSettle();
113 expect(callCount, equals(1));
114
115 await tester.pumpWidget(
116 Center(
117 child: AnimatedSize(
118 onEnd: handleEnd,
119 duration: const Duration(milliseconds: 200),
120 child: const SizedBox(width: 100.0, height: 100.0),
121 ),
122 ),
123 );
124
125 await tester.pumpAndSettle();
126 expect(callCount, equals(2));
127 });
128
129 testWidgets('clamps animated size to constraints', (WidgetTester tester) async {
130 await tester.pumpWidget(
131 const Center(
132 child: SizedBox(
133 width: 100.0,
134 height: 100.0,
135 child: AnimatedSize(
136 duration: Duration(milliseconds: 200),
137 child: SizedBox(width: 100.0, height: 100.0),
138 ),
139 ),
140 ),
141 );
142
143 RenderBox box = tester.renderObject(find.byType(AnimatedSize));
144 expect(box.size.width, equals(100.0));
145 expect(box.size.height, equals(100.0));
146
147 // Attempt to animate beyond the outer SizedBox.
148 await tester.pumpWidget(
149 const Center(
150 child: SizedBox(
151 width: 100.0,
152 height: 100.0,
153 child: AnimatedSize(
154 duration: Duration(milliseconds: 200),
155 child: SizedBox(width: 200.0, height: 200.0),
156 ),
157 ),
158 ),
159 );
160
161 // Verify that animated size is the same as the outer SizedBox.
162 await tester.pump(const Duration(milliseconds: 100));
163 box = tester.renderObject(find.byType(AnimatedSize));
164 expect(box.size.width, equals(100.0));
165 expect(box.size.height, equals(100.0));
166 });
167
168 testWidgets('tracks unstable child, then resumes animation when child stabilizes', (
169 WidgetTester tester,
170 ) async {
171 Future<void> pumpMillis(int millis) async {
172 await tester.pump(Duration(milliseconds: millis));
173 }
174
175 void verify({double? size, RenderAnimatedSizeState? state}) {
176 assert(size != null || state != null);
177 final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize));
178 if (size != null) {
179 expect(box.size.width, size);
180 expect(box.size.height, size);
181 }
182 if (state != null) {
183 expect(box.state, state);
184 }
185 }
186
187 await tester.pumpWidget(
188 Center(
189 child: AnimatedSize(
190 duration: const Duration(milliseconds: 200),
191 child: AnimatedContainer(
192 duration: const Duration(milliseconds: 100),
193 width: 100.0,
194 height: 100.0,
195 ),
196 ),
197 ),
198 );
199
200 verify(size: 100.0, state: RenderAnimatedSizeState.stable);
201
202 // Animate child size from 100 to 200 slowly (100ms).
203 await tester.pumpWidget(
204 Center(
205 child: AnimatedSize(
206 duration: const Duration(milliseconds: 200),
207 child: AnimatedContainer(
208 duration: const Duration(milliseconds: 100),
209 width: 200.0,
210 height: 200.0,
211 ),
212 ),
213 ),
214 );
215
216 // Make sure animation proceeds at child's pace, with AnimatedSize
217 // tightly tracking the child's size.
218 verify(state: RenderAnimatedSizeState.stable);
219 await pumpMillis(1); // register change
220 verify(state: RenderAnimatedSizeState.changed);
221 await pumpMillis(49);
222 verify(size: 150.0, state: RenderAnimatedSizeState.unstable);
223 await pumpMillis(50);
224 verify(size: 200.0, state: RenderAnimatedSizeState.unstable);
225
226 // Stabilize size
227 await pumpMillis(50);
228 verify(size: 200.0, state: RenderAnimatedSizeState.stable);
229
230 // Quickly (in 1ms) change size back to 100
231 await tester.pumpWidget(
232 Center(
233 child: AnimatedSize(
234 duration: const Duration(milliseconds: 200),
235 child: AnimatedContainer(
236 duration: const Duration(milliseconds: 1),
237 width: 100.0,
238 height: 100.0,
239 ),
240 ),
241 ),
242 );
243
244 verify(size: 200.0, state: RenderAnimatedSizeState.stable);
245 await pumpMillis(1); // register change
246 verify(state: RenderAnimatedSizeState.changed);
247 await pumpMillis(100);
248 verify(size: 150.0, state: RenderAnimatedSizeState.stable);
249 await pumpMillis(100);
250 verify(size: 100.0, state: RenderAnimatedSizeState.stable);
251 });
252
253 testWidgets('resyncs its animation controller', (WidgetTester tester) async {
254 await tester.pumpWidget(
255 const Center(
256 child: AnimatedSize(
257 duration: Duration(milliseconds: 200),
258 child: SizedBox(width: 100.0, height: 100.0),
259 ),
260 ),
261 );
262
263 await tester.pumpWidget(
264 const Center(
265 child: AnimatedSize(
266 duration: Duration(milliseconds: 200),
267 child: SizedBox(width: 200.0, height: 100.0),
268 ),
269 ),
270 );
271
272 await tester.pump(const Duration(milliseconds: 100));
273
274 final RenderBox box = tester.renderObject(find.byType(AnimatedSize));
275 expect(box.size.width, equals(150.0));
276 });
277
278 testWidgets('does not run animation unnecessarily', (WidgetTester tester) async {
279 await tester.pumpWidget(
280 const Center(
281 child: AnimatedSize(
282 duration: Duration(milliseconds: 200),
283 child: SizedBox(width: 100.0, height: 100.0),
284 ),
285 ),
286 );
287
288 for (int i = 0; i < 20; i++) {
289 final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize));
290 expect(box.size.width, 100.0);
291 expect(box.size.height, 100.0);
292 expect(box.state, RenderAnimatedSizeState.stable);
293 expect(box.isAnimating, false);
294 await tester.pump(const Duration(milliseconds: 10));
295 }
296 });
297
298 testWidgets('can set and update clipBehavior', (WidgetTester tester) async {
299 await tester.pumpWidget(
300 const Center(
301 child: AnimatedSize(
302 duration: Duration(milliseconds: 200),
303 child: SizedBox(width: 100.0, height: 100.0),
304 ),
305 ),
306 );
307
308 // By default, clipBehavior should be Clip.hardEdge
309 final RenderAnimatedSize renderObject = tester.renderObject(find.byType(AnimatedSize));
310 expect(renderObject.clipBehavior, equals(Clip.hardEdge));
311
312 for (final Clip clip in Clip.values) {
313 await tester.pumpWidget(
314 Center(
315 child: AnimatedSize(
316 duration: const Duration(milliseconds: 200),
317 clipBehavior: clip,
318 child: const SizedBox(width: 100.0, height: 100.0),
319 ),
320 ),
321 );
322 expect(renderObject.clipBehavior, clip);
323 }
324 });
325
326 testWidgets('works wrapped in IntrinsicHeight and Wrap', (WidgetTester tester) async {
327 Future<void> pumpWidget(Size size, [Duration? duration]) async {
328 return tester.pumpWidget(
329 Center(
330 child: IntrinsicHeight(
331 child: Wrap(
332 textDirection: TextDirection.ltr,
333 children: <Widget>[
334 AnimatedSize(
335 duration: const Duration(milliseconds: 200),
336 curve: Curves.easeInOutBack,
337 child: SizedBox(width: size.width, height: size.height),
338 ),
339 ],
340 ),
341 ),
342 ),
343 duration: duration,
344 );
345 }
346
347 await pumpWidget(const Size(100, 100));
348 expect(
349 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
350 const Size(100, 100),
351 );
352
353 await pumpWidget(const Size(150, 200));
354 expect(
355 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
356 const Size(100, 100),
357 );
358
359 // Each pump triggers verification of dry layout.
360 for (int total = 0; total < 200; total += 10) {
361 await tester.pump(const Duration(milliseconds: 10));
362 }
363 expect(
364 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
365 const Size(150, 200),
366 );
367
368 // Change every pump
369 await pumpWidget(const Size(100, 100));
370 expect(
371 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
372 const Size(150, 200),
373 );
374
375 await pumpWidget(const Size(111, 111), const Duration(milliseconds: 10));
376 expect(
377 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
378 const Size(111, 111),
379 );
380
381 await pumpWidget(const Size(222, 222), const Duration(milliseconds: 10));
382 expect(
383 tester.renderObject<RenderBox>(find.byType(IntrinsicHeight)).size,
384 const Size(222, 222),
385 );
386 });
387
388 testWidgets('re-attach with interrupted animation', (WidgetTester tester) async {
389 const Key key1 = ValueKey<String>('key1');
390 const Key key2 = ValueKey<String>('key2');
391 late StateSetter setState;
392 Size childSize = const Size.square(100);
393 final Widget animatedSize = Center(
394 key: GlobalKey(debugLabel: 'animated size'),
395 // This SizedBox creates a relayout boundary so _cleanRelayoutBoundary
396 // does not mark the descendant render objects below the relayout boundary
397 // dirty.
398 child: SizedBox.fromSize(
399 size: const Size.square(200),
400 child: Center(
401 child: AnimatedSize(
402 duration: const Duration(seconds: 1),
403 child: StatefulBuilder(
404 builder: (BuildContext context, StateSetter stateSetter) {
405 setState = stateSetter;
406 return SizedBox.fromSize(size: childSize);
407 },
408 ),
409 ),
410 ),
411 ),
412 );
413
414 await tester.pumpWidget(
415 Directionality(
416 textDirection: TextDirection.ltr,
417 child: Row(
418 children: <Widget>[
419 SizedBox(key: key1, height: 200, child: animatedSize),
420 const SizedBox(key: key2, height: 200),
421 ],
422 ),
423 ),
424 );
425
426 setState(() {
427 childSize = const Size.square(150);
428 });
429 // Kick off the resizing animation.
430 await tester.pump();
431
432 // Immediately reparent the AnimatedSize subtree to a different parent
433 // with the same incoming constraints.
434 await tester.pumpWidget(
435 Directionality(
436 textDirection: TextDirection.ltr,
437 child: Row(
438 children: <Widget>[
439 const SizedBox(key: key1, height: 200),
440 SizedBox(key: key2, height: 200, child: animatedSize),
441 ],
442 ),
443 ),
444 );
445
446 expect(
447 tester.renderObject<RenderBox>(find.byType(AnimatedSize)).size,
448 const Size.square(100),
449 );
450 await tester.pumpAndSettle();
451 // The animatedSize widget animates to the right size.
452 expect(
453 tester.renderObject<RenderBox>(find.byType(AnimatedSize)).size,
454 const Size.square(150),
455 );
456 });
457
458 testWidgets('disposes animation and controller', (WidgetTester tester) async {
459 await tester.pumpWidget(
460 const Center(
461 child: AnimatedSize(
462 duration: Duration(milliseconds: 200),
463 child: SizedBox(width: 100.0, height: 100.0),
464 ),
465 ),
466 );
467
468 final RenderAnimatedSize box = tester.renderObject(find.byType(AnimatedSize));
469
470 await tester.pumpWidget(const Center());
471
472 expect(box.debugAnimation, isNotNull);
473 expect(box.debugAnimation!.isDisposed, isTrue);
474 expect(box.debugController, isNotNull);
475 expect(
476 () => box.debugController!.dispose(),
477 throwsA(
478 isA<AssertionError>().having(
479 (AssertionError error) => error.message,
480 'message',
481 equalsIgnoringHashCodes(
482 'AnimationController.dispose() called more than once.\n'
483 'A given AnimationController cannot be disposed more than once.\n'
484 'The following AnimationController object was disposed multiple times:\n'
485 ' AnimationController#00000(⏮ 0.000; paused; DISPOSED)',
486 ),
487 ),
488 ),
489 );
490 });
491 });
492}
493