animated_size.dart 8.51 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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
    @required TickerProvider vsync,
    @required Duration duration,
79 80
    Curve curve = Curves.linear,
    AlignmentGeometry alignment = Alignment.center,
81
    TextDirection textDirection,
82
    RenderBox child,
83 84 85 86
  }) : assert(vsync != null),
       assert(duration != null),
       assert(curve != null),
       _vsync = vsync,
87
       super(child: child, alignment: alignment, textDirection: textDirection) {
88
    _controller = new AnimationController(
89 90
      vsync: vsync,
      duration: duration,
91 92 93 94 95 96 97 98 99 100 101 102
    )..addListener(() {
      if (_controller.value != _lastValue)
        markNeedsLayout();
    });
    _animation = new CurvedAnimation(
      parent: _controller,
      curve: curve
    );
  }

  AnimationController _controller;
  CurvedAnimation _animation;
103
  final SizeTween _sizeTween = new SizeTween();
104 105 106
  bool _hasVisualOverflow;
  double _lastValue;

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

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
  /// 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;
  }

  /// 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;
  }

132 133 134 135 136 137
  /// 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;

138 139 140 141 142 143 144 145 146 147 148
  /// 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);
  }

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
  @override
  void detach() {
    _controller.stop();
    super.detach();
  }

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

  @override
  void performLayout() {
    _lastValue = _controller.value;
    _hasVisualOverflow = false;

164 165
    if (child == null || constraints.isTight) {
      _controller.stop();
166
      size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
167 168
      _state = RenderAnimatedSizeState.start;
      child?.layout(constraints);
169 170 171 172 173
      return;
    }

    child.layout(constraints, parentUsesSize: true);

174 175
    assert(_state != null);
    switch (_state) {
176 177 178 179 180 181 182 183 184 185 186 187
      case RenderAnimatedSizeState.start:
        _layoutStart();
        break;
      case RenderAnimatedSizeState.stable:
        _layoutStable();
        break;
      case RenderAnimatedSizeState.changed:
        _layoutChanged();
        break;
      case RenderAnimatedSizeState.unstable:
        _layoutUnstable();
        break;
188 189
    }

190
    size = constraints.constrain(_animatedSize);
191 192 193 194 195 196 197
    alignChild();

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

198 199 200 201 202 203 204 205 206 207
  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() {
208
    _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
209 210 211 212 213 214 215 216 217 218
    _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) {
219
      _sizeTween.begin = size;
220
      _sizeTween.end = debugAdoptSize(child.size);
221 222 223 224
      _restartAnimation();
      _state = RenderAnimatedSizeState.changed;
    } else if (_controller.value == _controller.upperBound) {
      // Animation finished. Reset target sizes.
225
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
226 227
    } else if (!_controller.isAnimating) {
      _controller.forward(); // resume the animation after being detached
228 229 230 231 232 233 234 235 236 237 238 239
    }
  }

  /// 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.
240
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
241 242 243 244 245
      _restartAnimation();
      _state = RenderAnimatedSizeState.unstable;
    } else {
      // Child size stabilized.
      _state = RenderAnimatedSizeState.stable;
246 247
      if (!_controller.isAnimating)
        _controller.forward(); // resume the animation after being detached
248 249 250 251 252 253 254 255 256
    }
  }

  /// 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.
257
      _sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
258 259 260 261 262 263 264 265
      _restartAnimation();
    } else {
      // Child size stabilized.
      _controller.stop();
      _state = RenderAnimatedSizeState.stable;
    }
  }

266 267 268
  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && _hasVisualOverflow) {
269
      final Rect rect = Offset.zero & size;
270 271 272 273 274 275
      context.pushClipRect(needsCompositing, offset, rect, super.paint);
    } else {
      super.paint(context, offset);
    }
  }
}