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/animation.dart';
6import 'package:flutter/foundation.dart';
7
8import 'box.dart';
9import 'layer.dart';
10import 'object.dart';
11import 'shifted_box.dart';
12
13/// A [RenderAnimatedSize] can be in exactly one of these states.
14@visibleForTesting
15enum 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.
62class 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 switch (status) {
385 case AnimationStatus.completed:
386 _onEnd?.call();
387 case AnimationStatus.dismissed:
388 case AnimationStatus.forward:
389 case AnimationStatus.reverse:
390 }
391 }
392
393 @override
394 void paint(PaintingContext context, Offset offset) {
395 if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) {
396 final Rect rect = Offset.zero & size;
397 _clipRectLayer.layer = context.pushClipRect(
398 needsCompositing,
399 offset,
400 rect,
401 super.paint,
402 clipBehavior: clipBehavior,
403 oldLayer: _clipRectLayer.layer,
404 );
405 } else {
406 _clipRectLayer.layer = null;
407 super.paint(context, offset);
408 }
409 }
410
411 final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
412
413 @override
414 void dispose() {
415 _clipRectLayer.layer = null;
416 _controller.dispose();
417 _animation.dispose();
418 super.dispose();
419 }
420}
421