animated_size.dart 8.93 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/animation.dart';
6
import 'package:flutter/foundation.dart';
7
import 'package:flutter/scheduler.dart';
8 9 10 11 12

import 'box.dart';
import 'object.dart';
import 'shifted_box.dart';

13 14 15 16 17 18
/// A [RenderAnimatedSize] can be in exactly one of these states.
@visibleForTesting
enum RenderAnimatedSizeState {
  /// The initial state, when we do not yet know what the starting and target
  /// sizes are to animate.
  ///
19
  /// The next state is [stable].
20 21 22 23 24
  start,

  /// At this state the child's size is assumed to be stable and we are either
  /// animating, or waiting for the child's size to change.
  ///
25 26
  /// If the child's size changes, the state will become [changed]. Otherwise,
  /// it remains [stable].
27 28 29 30 31
  stable,

  /// At this state we know that the child has changed once after being assumed
  /// [stable].
  ///
32
  /// The next state will be one of:
33
  ///
34 35 36 37 38
  /// * [stable] if the child's size stabilized immediately. This is a signal
  ///   for the render object to begin animating the size towards the child's new
  ///   size.
  ///
  /// * [unstable] if the child's size continues to change.
39 40
  changed,

41 42
  /// At this state the child's size is assumed to be unstable (changing each
  /// frame).
43
  ///
44 45
  /// Instead of chasing the child's size in this state, the render object
  /// tightly tracks the child's size until it stabilizes.
46
  ///
47 48 49
  /// The render object remains in this state until a frame where the child's
  /// size remains the same as the previous frame. At that time, the next state
  /// is [stable].
50 51 52
  unstable,
}

53
/// A render object that animates its size to its child's size over a given
54 55 56 57 58
/// [duration] and with a given [curve]. If the child's size itself animates
/// (i.e. if it changes size two frames in a row, as opposed to abruptly
/// changing size in one frame then remaining that size in subsequent frames),
/// this render object sizes itself to fit the child instead of animating
/// itself.
59
///
60 61
/// When the child overflows the current animated size of this render object, it
/// is clipped.
62 63
class RenderAnimatedSize extends RenderAligningShiftedBox {
  /// Creates a render object that animates its size to match its child.
64 65 66
  /// The [duration] and [curve] arguments define the animation.
  ///
  /// The [alignment] argument is used to align the child when the parent is not
67 68
  /// (yet) the same size as the child.
  ///
69 70 71 72 73 74 75
  /// The [duration] is required.
  ///
  /// The [vsync] should specify a [TickerProvider] for the animation
  /// controller.
  ///
  /// The arguments [duration], [curve], [alignment], and [vsync] must
  /// not be null.
76
  RenderAnimatedSize({
77 78 79
    required TickerProvider vsync,
    required Duration duration,
    Duration? reverseDuration,
80 81
    Curve curve = Curves.linear,
    AlignmentGeometry alignment = Alignment.center,
82 83
    TextDirection? textDirection,
    RenderBox? child,
84 85 86 87
  }) : assert(vsync != null),
       assert(duration != null),
       assert(curve != null),
       _vsync = vsync,
88
       super(child: child, alignment: alignment, textDirection: textDirection) {
89
    _controller = AnimationController(
90 91
      vsync: vsync,
      duration: duration,
92
      reverseDuration: reverseDuration,
93 94 95 96
    )..addListener(() {
      if (_controller.value != _lastValue)
        markNeedsLayout();
    });
97
    _animation = CurvedAnimation(
98
      parent: _controller,
99
      curve: curve,
100 101 102
    );
  }

103 104
  late final AnimationController _controller;
  late final CurvedAnimation _animation;
105
  final SizeTween _sizeTween = SizeTween();
106 107
  late bool _hasVisualOverflow;
  double? _lastValue;
108

109 110 111 112 113 114 115
  /// The state this size animation is in.
  ///
  /// See [RenderAnimatedSizeState] for possible states.
  @visibleForTesting
  RenderAnimatedSizeState get state => _state;
  RenderAnimatedSizeState _state = RenderAnimatedSizeState.start;

116
  /// The duration of the animation.
117
  Duration get duration => _controller.duration!;
118 119 120 121 122 123 124
  set duration(Duration value) {
    assert(value != null);
    if (value == _controller.duration)
      return;
    _controller.duration = value;
  }

125
  /// The duration of the animation when running in reverse.
126 127
  Duration? get reverseDuration => _controller.reverseDuration;
  set reverseDuration(Duration? value) {
128 129 130 131 132
    if (value == _controller.reverseDuration)
      return;
    _controller.reverseDuration = value;
  }

133 134 135 136 137 138 139 140 141
  /// The curve of the animation.
  Curve get curve => _animation.curve;
  set curve(Curve value) {
    assert(value != null);
    if (value == _animation.curve)
      return;
    _animation.curve = value;
  }

142 143 144 145 146 147
  /// Whether the size is being currently animated towards the child's size.
  ///
  /// See [RenderAnimatedSizeState] for situations when we may not be animating
  /// the size.
  bool get isAnimating => _controller.isAnimating;

148 149 150 151 152 153 154 155 156 157 158
  /// The [TickerProvider] for the [AnimationController] that runs the animation.
  TickerProvider get vsync => _vsync;
  TickerProvider _vsync;
  set vsync(TickerProvider value) {
    assert(value != null);
    if (value == _vsync)
      return;
    _vsync = value;
    _controller.resync(vsync);
  }

159 160 161 162 163 164
  @override
  void detach() {
    _controller.stop();
    super.detach();
  }

165
  Size? get _animatedSize {
166 167 168 169 170 171 172
    return _sizeTween.evaluate(_animation);
  }

  @override
  void performLayout() {
    _lastValue = _controller.value;
    _hasVisualOverflow = false;
173
    final BoxConstraints constraints = this.constraints;
174 175
    if (child == null || constraints.isTight) {
      _controller.stop();
176
      size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
177 178
      _state = RenderAnimatedSizeState.start;
      child?.layout(constraints);
179 180 181
      return;
    }

182
    child!.layout(constraints, parentUsesSize: true);
183

184 185
    assert(_state != null);
    switch (_state) {
186 187 188 189 190 191 192 193 194 195 196 197
      case RenderAnimatedSizeState.start:
        _layoutStart();
        break;
      case RenderAnimatedSizeState.stable:
        _layoutStable();
        break;
      case RenderAnimatedSizeState.changed:
        _layoutChanged();
        break;
      case RenderAnimatedSizeState.unstable:
        _layoutUnstable();
        break;
198 199
    }

200
    size = constraints.constrain(_animatedSize!);
201 202
    alignChild();

203 204
    if (size.width < _sizeTween.end!.width ||
        size.height < _sizeTween.end!.height)
205 206 207
      _hasVisualOverflow = true;
  }

208 209 210 211 212 213 214 215 216 217
  void _restartAnimation() {
    _lastValue = 0.0;
    _controller.forward(from: 0.0);
  }

  /// Laying out the child for the first time.
  ///
  /// We have the initial size to animate from, but we do not have the target
  /// size to animate to, so we set both ends to child's size.
  void _layoutStart() {
218
    _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
219 220 221 222 223 224 225 226 227
    _state = RenderAnimatedSizeState.stable;
  }

  /// At this state we're assuming the child size is stable and letting the
  /// animation run its course.
  ///
  /// If during animation the size of the child changes we restart the
  /// animation.
  void _layoutStable() {
228
    if (_sizeTween.end != child!.size) {
229
      _sizeTween.begin = size;
230
      _sizeTween.end = debugAdoptSize(child!.size);
231 232 233 234
      _restartAnimation();
      _state = RenderAnimatedSizeState.changed;
    } else if (_controller.value == _controller.upperBound) {
      // Animation finished. Reset target sizes.
235
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
236 237
    } else if (!_controller.isAnimating) {
      _controller.forward(); // resume the animation after being detached
238 239 240 241 242 243 244 245 246 247
    }
  }

  /// This state indicates that the size of the child changed once after being
  /// considered stable.
  ///
  /// If the child stabilizes immediately, we go back to stable state. If it
  /// changes again, we match the child's size, restart animation and go to
  /// unstable state.
  void _layoutChanged() {
248
    if (_sizeTween.end != child!.size) {
249
      // Child size changed again. Match the child's size and restart animation.
250
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
251 252 253 254 255
      _restartAnimation();
      _state = RenderAnimatedSizeState.unstable;
    } else {
      // Child size stabilized.
      _state = RenderAnimatedSizeState.stable;
256 257
      if (!_controller.isAnimating)
        _controller.forward(); // resume the animation after being detached
258 259 260 261 262 263 264
    }
  }

  /// The child's size is not stable.
  ///
  /// Continue tracking the child's size until is stabilizes.
  void _layoutUnstable() {
265
    if (_sizeTween.end != child!.size) {
266
      // Still unstable. Continue tracking the child.
267
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
268 269 270 271 272 273 274 275
      _restartAnimation();
    } else {
      // Child size stabilized.
      _controller.stop();
      _state = RenderAnimatedSizeState.stable;
    }
  }

276 277 278
  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && _hasVisualOverflow) {
279
      final Rect rect = Offset.zero & size;
280 281 282 283 284 285
      context.pushClipRect(needsCompositing, offset, rect, super.paint);
    } else {
      super.paint(context, offset);
    }
  }
}