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

5 6
// @dart = 2.8

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

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

15 16 17 18 19 20
/// 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.
  ///
21
  /// The next state is [stable].
22 23 24 25 26
  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.
  ///
27 28
  /// If the child's size changes, the state will become [changed]. Otherwise,
  /// it remains [stable].
29 30 31 32 33
  stable,

  /// At this state we know that the child has changed once after being assumed
  /// [stable].
  ///
34
  /// The next state will be one of:
35
  ///
36 37 38 39 40
  /// * [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.
41 42
  changed,

43 44
  /// At this state the child's size is assumed to be unstable (changing each
  /// frame).
45
  ///
46 47
  /// Instead of chasing the child's size in this state, the render object
  /// tightly tracks the child's size until it stabilizes.
48
  ///
49 50 51
  /// 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].
52 53 54
  unstable,
}

55
/// A render object that animates its size to its child's size over a given
56 57 58 59 60
/// [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.
61
///
62 63
/// When the child overflows the current animated size of this render object, it
/// is clipped.
64 65
class RenderAnimatedSize extends RenderAligningShiftedBox {
  /// Creates a render object that animates its size to match its child.
66 67 68
  /// The [duration] and [curve] arguments define the animation.
  ///
  /// The [alignment] argument is used to align the child when the parent is not
69 70
  /// (yet) the same size as the child.
  ///
71 72 73 74 75 76 77
  /// 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.
78
  RenderAnimatedSize({
79 80
    @required TickerProvider vsync,
    @required Duration duration,
81
    Duration reverseDuration,
82 83
    Curve curve = Curves.linear,
    AlignmentGeometry alignment = Alignment.center,
84
    TextDirection textDirection,
85
    RenderBox child,
86 87 88 89
  }) : assert(vsync != null),
       assert(duration != null),
       assert(curve != null),
       _vsync = vsync,
90
       super(child: child, alignment: alignment, textDirection: textDirection) {
91
    _controller = AnimationController(
92 93
      vsync: vsync,
      duration: duration,
94
      reverseDuration: reverseDuration,
95 96 97 98
    )..addListener(() {
      if (_controller.value != _lastValue)
        markNeedsLayout();
    });
99
    _animation = CurvedAnimation(
100
      parent: _controller,
101
      curve: curve,
102 103 104 105 106
    );
  }

  AnimationController _controller;
  CurvedAnimation _animation;
107
  final SizeTween _sizeTween = SizeTween();
108 109 110
  bool _hasVisualOverflow;
  double _lastValue;

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

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

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

135 136 137 138 139 140 141 142 143
  /// 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;
  }

144 145 146 147 148 149
  /// 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;

150 151 152 153 154 155 156 157 158 159 160
  /// 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);
  }

161 162 163 164 165 166 167 168 169 170 171 172 173 174
  @override
  void detach() {
    _controller.stop();
    super.detach();
  }

  Size get _animatedSize {
    return _sizeTween.evaluate(_animation);
  }

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

    child.layout(constraints, parentUsesSize: true);

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

202
    size = constraints.constrain(_animatedSize);
203 204 205 206 207 208 209
    alignChild();

    if (size.width < _sizeTween.end.width ||
        size.height < _sizeTween.end.height)
      _hasVisualOverflow = true;
  }

210 211 212 213 214 215 216 217 218 219
  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() {
220
    _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
221 222 223 224 225 226 227 228 229 230
    _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() {
    if (_sizeTween.end != child.size) {
231
      _sizeTween.begin = size;
232
      _sizeTween.end = debugAdoptSize(child.size);
233 234 235 236
      _restartAnimation();
      _state = RenderAnimatedSizeState.changed;
    } else if (_controller.value == _controller.upperBound) {
      // Animation finished. Reset target sizes.
237
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
238 239
    } else if (!_controller.isAnimating) {
      _controller.forward(); // resume the animation after being detached
240 241 242 243 244 245 246 247 248 249 250 251
    }
  }

  /// 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() {
    if (_sizeTween.end != child.size) {
      // Child size changed again. Match the child's size and restart animation.
252
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
253 254 255 256 257
      _restartAnimation();
      _state = RenderAnimatedSizeState.unstable;
    } else {
      // Child size stabilized.
      _state = RenderAnimatedSizeState.stable;
258 259
      if (!_controller.isAnimating)
        _controller.forward(); // resume the animation after being detached
260 261 262 263 264 265 266 267 268
    }
  }

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

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