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/animation.dart'; |
6 | import 'package:flutter/foundation.dart'; |
7 | |
8 | import 'box.dart'; |
9 | import 'layer.dart'; |
10 | import 'object.dart'; |
11 | import 'shifted_box.dart'; |
12 | |
13 | /// A [RenderAnimatedSize] can be in exactly one of these states. |
14 | @visibleForTesting |
15 | enum RenderAnimatedSizeState { |
16 | /// The initial state, when we do not yet know what the starting and target |
17 | /// sizes are to animate. |
18 | /// |
19 | /// The next state is [stable]. |
20 | start, |
21 | |
22 | /// At this state the child's size is assumed to be stable and we are either |
23 | /// animating, or waiting for the child's size to change. |
24 | /// |
25 | /// If the child's size changes, the state will become [changed]. Otherwise, |
26 | /// it remains [stable]. |
27 | stable, |
28 | |
29 | /// At this state we know that the child has changed once after being assumed |
30 | /// [stable]. |
31 | /// |
32 | /// The next state will be one of: |
33 | /// |
34 | /// * [stable] if the child's size stabilized immediately. This is a signal |
35 | /// for the render object to begin animating the size towards the child's new |
36 | /// size. |
37 | /// |
38 | /// * [unstable] if the child's size continues to change. |
39 | changed, |
40 | |
41 | /// At this state the child's size is assumed to be unstable (changing each |
42 | /// frame). |
43 | /// |
44 | /// Instead of chasing the child's size in this state, the render object |
45 | /// tightly tracks the child's size until it stabilizes. |
46 | /// |
47 | /// The render object remains in this state until a frame where the child's |
48 | /// size remains the same as the previous frame. At that time, the next state |
49 | /// is [stable]. |
50 | unstable, |
51 | } |
52 | |
53 | /// A render object that animates its size to its child's size over a given |
54 | /// [duration] and with a given [curve]. If the child's size itself animates |
55 | /// (i.e. if it changes size two frames in a row, as opposed to abruptly |
56 | /// changing size in one frame then remaining that size in subsequent frames), |
57 | /// this render object sizes itself to fit the child instead of animating |
58 | /// itself. |
59 | /// |
60 | /// When the child overflows the current animated size of this render object, it |
61 | /// is clipped. |
62 | class RenderAnimatedSize extends RenderAligningShiftedBox { |
63 | /// Creates a render object that animates its size to match its child. |
64 | /// The [duration] and [curve] arguments define the animation. |
65 | /// |
66 | /// The [alignment] argument is used to align the child when the parent is not |
67 | /// (yet) the same size as the child. |
68 | /// |
69 | /// The [duration] is required. |
70 | /// |
71 | /// The [vsync] should specify a [TickerProvider] for the animation |
72 | /// controller. |
73 | /// |
74 | /// The arguments [duration], [curve], [alignment], and [vsync] must |
75 | /// not be null. |
76 | RenderAnimatedSize({ |
77 | required TickerProvider vsync, |
78 | required Duration duration, |
79 | Duration? reverseDuration, |
80 | Curve curve = Curves.linear, |
81 | super.alignment, |
82 | super.textDirection, |
83 | super.child, |
84 | Clip clipBehavior = Clip.hardEdge, |
85 | VoidCallback? onEnd, |
86 | }) : _vsync = vsync, |
87 | _clipBehavior = clipBehavior { |
88 | _controller = AnimationController( |
89 | vsync: vsync, |
90 | duration: duration, |
91 | reverseDuration: reverseDuration, |
92 | )..addListener(() { |
93 | if (_controller.value != _lastValue) { |
94 | markNeedsLayout(); |
95 | } |
96 | }); |
97 | _animation = CurvedAnimation( |
98 | parent: _controller, |
99 | curve: curve, |
100 | ); |
101 | _onEnd = onEnd; |
102 | } |
103 | |
104 | /// When asserts are enabled, returns the animation controller that is used |
105 | /// to drive the resizing. |
106 | /// |
107 | /// Otherwise, returns null. |
108 | /// |
109 | /// This getter is intended for use in framework unit tests. Applications must |
110 | /// not depend on its value. |
111 | @visibleForTesting |
112 | AnimationController? get debugController { |
113 | AnimationController? controller; |
114 | assert(() { |
115 | controller = _controller; |
116 | return true; |
117 | }()); |
118 | return controller; |
119 | } |
120 | |
121 | /// When asserts are enabled, returns the animation that drives the resizing. |
122 | /// |
123 | /// Otherwise, returns null. |
124 | /// |
125 | /// This getter is intended for use in framework unit tests. Applications must |
126 | /// not depend on its value. |
127 | @visibleForTesting |
128 | CurvedAnimation? get debugAnimation { |
129 | CurvedAnimation? animation; |
130 | assert(() { |
131 | animation = _animation; |
132 | return true; |
133 | }()); |
134 | return animation; |
135 | } |
136 | |
137 | late final AnimationController _controller; |
138 | late final CurvedAnimation _animation; |
139 | |
140 | final SizeTween _sizeTween = SizeTween(); |
141 | late bool _hasVisualOverflow; |
142 | double? _lastValue; |
143 | |
144 | /// The state this size animation is in. |
145 | /// |
146 | /// See [RenderAnimatedSizeState] for possible states. |
147 | @visibleForTesting |
148 | RenderAnimatedSizeState get state => _state; |
149 | RenderAnimatedSizeState _state = RenderAnimatedSizeState.start; |
150 | |
151 | /// The duration of the animation. |
152 | Duration get duration => _controller.duration!; |
153 | set duration(Duration value) { |
154 | if (value == _controller.duration) { |
155 | return; |
156 | } |
157 | _controller.duration = value; |
158 | } |
159 | |
160 | /// The duration of the animation when running in reverse. |
161 | Duration? get reverseDuration => _controller.reverseDuration; |
162 | set reverseDuration(Duration? value) { |
163 | if (value == _controller.reverseDuration) { |
164 | return; |
165 | } |
166 | _controller.reverseDuration = value; |
167 | } |
168 | |
169 | /// The curve of the animation. |
170 | Curve get curve => _animation.curve; |
171 | set curve(Curve value) { |
172 | if (value == _animation.curve) { |
173 | return; |
174 | } |
175 | _animation.curve = value; |
176 | } |
177 | |
178 | /// {@macro flutter.material.Material.clipBehavior} |
179 | /// |
180 | /// Defaults to [Clip.hardEdge]. |
181 | Clip get clipBehavior => _clipBehavior; |
182 | Clip _clipBehavior = Clip.hardEdge; |
183 | set clipBehavior(Clip value) { |
184 | if (value != _clipBehavior) { |
185 | _clipBehavior = value; |
186 | markNeedsPaint(); |
187 | markNeedsSemanticsUpdate(); |
188 | } |
189 | } |
190 | |
191 | /// Whether the size is being currently animated towards the child's size. |
192 | /// |
193 | /// See [RenderAnimatedSizeState] for situations when we may not be animating |
194 | /// the size. |
195 | bool get isAnimating => _controller.isAnimating; |
196 | |
197 | /// The [TickerProvider] for the [AnimationController] that runs the animation. |
198 | TickerProvider get vsync => _vsync; |
199 | TickerProvider _vsync; |
200 | set vsync(TickerProvider value) { |
201 | if (value == _vsync) { |
202 | return; |
203 | } |
204 | _vsync = value; |
205 | _controller.resync(vsync); |
206 | } |
207 | |
208 | /// Called every time an animation completes. |
209 | /// |
210 | /// This can be useful to trigger additional actions (e.g. another animation) |
211 | /// at the end of the current animation. |
212 | VoidCallback? get onEnd => _onEnd; |
213 | VoidCallback? _onEnd; |
214 | set onEnd(VoidCallback? value) { |
215 | if (value == _onEnd) { |
216 | return; |
217 | } |
218 | _onEnd = value; |
219 | } |
220 | |
221 | @override |
222 | void attach(PipelineOwner owner) { |
223 | super.attach(owner); |
224 | switch (state) { |
225 | case RenderAnimatedSizeState.start: |
226 | case RenderAnimatedSizeState.stable: |
227 | break; |
228 | case RenderAnimatedSizeState.changed: |
229 | case RenderAnimatedSizeState.unstable: |
230 | // Call markNeedsLayout in case the RenderObject isn't marked dirty |
231 | // already, to resume interrupted resizing animation. |
232 | markNeedsLayout(); |
233 | } |
234 | _controller.addStatusListener(_animationStatusListener); |
235 | } |
236 | |
237 | @override |
238 | void detach() { |
239 | _controller.stop(); |
240 | _controller.removeStatusListener(_animationStatusListener); |
241 | super.detach(); |
242 | } |
243 | |
244 | Size? get _animatedSize { |
245 | return _sizeTween.evaluate(_animation); |
246 | } |
247 | |
248 | @override |
249 | void performLayout() { |
250 | _lastValue = _controller.value; |
251 | _hasVisualOverflow = false; |
252 | final BoxConstraints constraints = this.constraints; |
253 | if (child == null || constraints.isTight) { |
254 | _controller.stop(); |
255 | size = _sizeTween.begin = _sizeTween.end = constraints.smallest; |
256 | _state = RenderAnimatedSizeState.start; |
257 | child?.layout(constraints); |
258 | return; |
259 | } |
260 | |
261 | child!.layout(constraints, parentUsesSize: true); |
262 | |
263 | switch (_state) { |
264 | case RenderAnimatedSizeState.start: |
265 | _layoutStart(); |
266 | case RenderAnimatedSizeState.stable: |
267 | _layoutStable(); |
268 | case RenderAnimatedSizeState.changed: |
269 | _layoutChanged(); |
270 | case RenderAnimatedSizeState.unstable: |
271 | _layoutUnstable(); |
272 | } |
273 | |
274 | size = constraints.constrain(_animatedSize!); |
275 | alignChild(); |
276 | |
277 | if (size.width < _sizeTween.end!.width || |
278 | size.height < _sizeTween.end!.height) { |
279 | _hasVisualOverflow = true; |
280 | } |
281 | } |
282 | |
283 | @override |
284 | @protected |
285 | Size computeDryLayout(covariant BoxConstraints constraints) { |
286 | if (child == null || constraints.isTight) { |
287 | return constraints.smallest; |
288 | } |
289 | |
290 | // This simplified version of performLayout only calculates the current |
291 | // size without modifying global state. See performLayout for comments |
292 | // explaining the rational behind the implementation. |
293 | final Size childSize = child!.getDryLayout(constraints); |
294 | switch (_state) { |
295 | case RenderAnimatedSizeState.start: |
296 | return constraints.constrain(childSize); |
297 | case RenderAnimatedSizeState.stable: |
298 | if (_sizeTween.end != childSize) { |
299 | return constraints.constrain(size); |
300 | } else if (_controller.value == _controller.upperBound) { |
301 | return constraints.constrain(childSize); |
302 | } |
303 | case RenderAnimatedSizeState.unstable: |
304 | case RenderAnimatedSizeState.changed: |
305 | if (_sizeTween.end != childSize) { |
306 | return constraints.constrain(childSize); |
307 | } |
308 | } |
309 | |
310 | return constraints.constrain(_animatedSize!); |
311 | } |
312 | |
313 | void _restartAnimation() { |
314 | _lastValue = 0.0; |
315 | _controller.forward(from: 0.0); |
316 | } |
317 | |
318 | /// Laying out the child for the first time. |
319 | /// |
320 | /// We have the initial size to animate from, but we do not have the target |
321 | /// size to animate to, so we set both ends to child's size. |
322 | void _layoutStart() { |
323 | _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size); |
324 | _state = RenderAnimatedSizeState.stable; |
325 | } |
326 | |
327 | /// At this state we're assuming the child size is stable and letting the |
328 | /// animation run its course. |
329 | /// |
330 | /// If during animation the size of the child changes we restart the |
331 | /// animation. |
332 | void _layoutStable() { |
333 | if (_sizeTween.end != child!.size) { |
334 | _sizeTween.begin = size; |
335 | _sizeTween.end = debugAdoptSize(child!.size); |
336 | _restartAnimation(); |
337 | _state = RenderAnimatedSizeState.changed; |
338 | } else if (_controller.value == _controller.upperBound) { |
339 | // Animation finished. Reset target sizes. |
340 | _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size); |
341 | } else if (!_controller.isAnimating) { |
342 | _controller.forward(); // resume the animation after being detached |
343 | } |
344 | } |
345 | |
346 | /// This state indicates that the size of the child changed once after being |
347 | /// considered stable. |
348 | /// |
349 | /// If the child stabilizes immediately, we go back to stable state. If it |
350 | /// changes again, we match the child's size, restart animation and go to |
351 | /// unstable state. |
352 | void _layoutChanged() { |
353 | if (_sizeTween.end != child!.size) { |
354 | // Child size changed again. Match the child's size and restart animation. |
355 | _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size); |
356 | _restartAnimation(); |
357 | _state = RenderAnimatedSizeState.unstable; |
358 | } else { |
359 | // Child size stabilized. |
360 | _state = RenderAnimatedSizeState.stable; |
361 | if (!_controller.isAnimating) { |
362 | // Resume the animation after being detached. |
363 | _controller.forward(); |
364 | } |
365 | } |
366 | } |
367 | |
368 | /// The child's size is not stable. |
369 | /// |
370 | /// Continue tracking the child's size until is stabilizes. |
371 | void _layoutUnstable() { |
372 | if (_sizeTween.end != child!.size) { |
373 | // Still unstable. Continue tracking the child. |
374 | _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size); |
375 | _restartAnimation(); |
376 | } else { |
377 | // Child size stabilized. |
378 | _controller.stop(); |
379 | _state = RenderAnimatedSizeState.stable; |
380 | } |
381 | } |
382 | |
383 | void _animationStatusListener(AnimationStatus status) { |
384 | if (status.isCompleted) { |
385 | _onEnd?.call(); |
386 | } |
387 | } |
388 | |
389 | @override |
390 | void paint(PaintingContext context, Offset offset) { |
391 | if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) { |
392 | final Rect rect = Offset.zero & size; |
393 | _clipRectLayer.layer = context.pushClipRect( |
394 | needsCompositing, |
395 | offset, |
396 | rect, |
397 | super.paint, |
398 | clipBehavior: clipBehavior, |
399 | oldLayer: _clipRectLayer.layer, |
400 | ); |
401 | } else { |
402 | _clipRectLayer.layer = null; |
403 | super.paint(context, offset); |
404 | } |
405 | } |
406 | |
407 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
408 | |
409 | @override |
410 | void dispose() { |
411 | _clipRectLayer.layer = null; |
412 | _controller.dispose(); |
413 | _animation.dispose(); |
414 | super.dispose(); |
415 | } |
416 | } |
417 | |