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