ticker_provider.dart 9.78 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';

import 'framework.dart';

export 'package:flutter/scheduler.dart' show TickerProvider;

/// Enables or disables tickers (and thus animation controllers) in the widget
/// subtree.
///
/// This only works if [AnimationController] objects are created using
/// widget-aware ticker providers. For example, using a
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
18
class TickerMode extends StatelessWidget {
19 20 21
  /// Creates a widget that enables or disables tickers.
  ///
  /// The [enabled] argument must not be null.
22
  const TickerMode({
23 24 25
    Key? key,
    required this.enabled,
    required this.child,
26
  }) : assert(enabled != null),
27
       super(key: key);
28

29 30 31 32
  /// The requested ticker mode for this subtree.
  ///
  /// The effective ticker mode of this subtree may differ from this value
  /// if there is an ancestor [TickerMode] with this field set to false.
33
  ///
34 35
  /// If true and all ancestor [TickerMode]s are also enabled, then tickers in
  /// this subtree will tick.
36
  ///
37 38 39
  /// If false, then tickers in this subtree will not tick regardless of any
  /// ancestor [TickerMode]s. Animations driven by such tickers are not paused,
  /// they just don't call their callbacks. Time still elapses.
40 41
  final bool enabled;

42 43
  /// The widget below this widget in the tree.
  ///
44
  /// {@macro flutter.widgets.child}
45 46
  final Widget child;

47 48 49 50 51 52 53
  /// Whether tickers in the given subtree should be enabled or disabled.
  ///
  /// This is used automatically by [TickerProviderStateMixin] and
  /// [SingleTickerProviderStateMixin] to decide if their tickers should be
  /// enabled or disabled.
  ///
  /// In the absence of a [TickerMode] widget, this function defaults to true.
54 55 56 57 58 59
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// bool tickingEnabled = TickerMode.of(context);
  /// ```
60
  static bool of(BuildContext context) {
61
    final _EffectiveTickerMode? widget = context.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
62 63 64 65
    return widget?.enabled ?? true;
  }

  @override
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
  Widget build(BuildContext context) {
    return _EffectiveTickerMode(
      enabled: enabled && TickerMode.of(context),
      child: child,
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(FlagProperty('requested mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
  }
}

class _EffectiveTickerMode extends InheritedWidget {
  const _EffectiveTickerMode({
82 83 84
    Key? key,
    required this.enabled,
    required Widget child,
85 86 87 88 89 90 91
  }) : assert(enabled != null),
        super(key: key, child: child);

  final bool enabled;

  @override
  bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
92 93

  @override
94 95
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
96
    properties.add(FlagProperty('effective mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
97 98 99 100 101 102 103 104 105 106 107 108 109
  }
}

/// Provides a single [Ticker] that is configured to only tick while the current
/// tree is enabled, as defined by [TickerMode].
///
/// To create the [AnimationController] in a [State] that only uses a single
/// [AnimationController], mix in this class, then pass `vsync: this`
/// to the animation controller constructor.
///
/// This mixin only supports vending a single ticker. If you might have multiple
/// [AnimationController] objects over the lifetime of the [State], use a full
/// [TickerProviderStateMixin] instead.
110
@optionalTypeArgs
111
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
112
  Ticker? _ticker;
113 114 115 116 117 118

  @override
  Ticker createTicker(TickerCallback onTick) {
    assert(() {
      if (_ticker == null)
        return true;
119 120 121 122 123 124 125 126 127
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
        ErrorDescription('A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
        ErrorHint(
          'If a State is used for multiple AnimationController objects, or if it is passed to other '
          'objects and those objects might use it more than one time in total, then instead of '
          'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'
        )
      ]);
128
    }());
129
    _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
130 131 132 133
    // We assume that this is called from initState, build, or some sort of
    // event handler, and that thus TickerMode.of(context) would return true. We
    // can't actually check that here because if we're in initState then we're
    // not allowed to do inheritance checks yet.
134
    return _ticker!;
135 136 137 138 139
  }

  @override
  void dispose() {
    assert(() {
140
      if (_ticker == null || !_ticker!.isActive)
141
        return true;
142 143 144 145 146 147 148 149 150 151 152 153
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('$this was disposed with an active Ticker.'),
        ErrorDescription(
          '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
          'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
          'be disposed before calling super.dispose().'
        ),
        ErrorHint(
          'Tickers used by AnimationControllers '
          'should be disposed by calling dispose() on the AnimationController itself. '
          'Otherwise, the ticker will leak.'
        ),
154
        _ticker!.describeForError('The offending ticker was')
155
      ]);
156
    }());
157 158 159 160
    super.dispose();
  }

  @override
161
  void didChangeDependencies() {
162
    if (_ticker != null)
163
      _ticker!.muted = !TickerMode.of(context);
164
    super.didChangeDependencies();
165 166 167
  }

  @override
168 169
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
170
    String? tickerDescription;
171
    if (_ticker != null) {
172
      if (_ticker!.isActive && _ticker!.muted)
173
        tickerDescription = 'active but muted';
174
      else if (_ticker!.isActive)
175
        tickerDescription = 'active';
176
      else if (_ticker!.muted)
177
        tickerDescription = 'inactive and muted';
178
      else
179
        tickerDescription = 'inactive';
180
    }
181
    properties.add(DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null));
182 183 184 185 186 187 188 189 190 191 192 193 194
  }
}

/// Provides [Ticker] objects that are configured to only tick while the current
/// tree is enabled, as defined by [TickerMode].
///
/// To create an [AnimationController] in a class that uses this mixin, pass
/// `vsync: this` to the animation controller constructor whenever you
/// create a new animation controller.
///
/// If you only have a single [Ticker] (for example only a single
/// [AnimationController]) for the lifetime of your [State], then using a
/// [SingleTickerProviderStateMixin] is more efficient. This is the common case.
195
@optionalTypeArgs
196
mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
197
  Set<Ticker>? _tickers;
198 199 200

  @override
  Ticker createTicker(TickerCallback onTick) {
201
    _tickers ??= <_WidgetTicker>{};
202
    final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: 'created by $this');
203
    _tickers!.add(result);
204 205 206 207 208
    return result;
  }

  void _removeTicker(_WidgetTicker ticker) {
    assert(_tickers != null);
209 210
    assert(_tickers!.contains(ticker));
    _tickers!.remove(ticker);
211 212 213 214 215 216
  }

  @override
  void dispose() {
    assert(() {
      if (_tickers != null) {
217
        for (final Ticker ticker in _tickers!) {
218
          if (ticker.isActive) {
219 220 221 222 223 224 225 226 227 228 229 230 231 232
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary('$this was disposed with an active Ticker.'),
              ErrorDescription(
                '$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
                'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
                'be disposed before calling super.dispose().'
              ),
              ErrorHint(
                'Tickers used by AnimationControllers '
                'should be disposed by calling dispose() on the AnimationController itself. '
                'Otherwise, the ticker will leak.'
              ),
              ticker.describeForError('The offending ticker was'),
            ]);
233 234 235 236
          }
        }
      }
      return true;
237
    }());
238 239 240 241
    super.dispose();
  }

  @override
242
  void didChangeDependencies() {
243 244
    final bool muted = !TickerMode.of(context);
    if (_tickers != null) {
245
      for (final Ticker ticker in _tickers!) {
246
        ticker.muted = muted;
247
      }
248
    }
249
    super.didChangeDependencies();
250 251 252
  }

  @override
253 254
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
255
    properties.add(DiagnosticsProperty<Set<Ticker>>(
256 257 258
      'tickers',
      _tickers,
      description: _tickers != null ?
259
        'tracking ${_tickers!.length} ticker${_tickers!.length == 1 ? "" : "s"}' :
260 261 262
        null,
      defaultValue: null,
    ));
263 264 265 266 267 268 269 270
  }
}

// This class should really be called _DisposingTicker or some such, but this
// class name leaks into stack traces and error messages and that name would be
// confusing. Instead we use the less precise but more anodyne "_WidgetTicker",
// which attracts less attention.
class _WidgetTicker extends Ticker {
271
  _WidgetTicker(TickerCallback onTick, this._creator, { String? debugLabel }) : super(onTick, debugLabel: debugLabel);
272

273
  final TickerProviderStateMixin _creator;
274 275 276 277 278 279 280

  @override
  void dispose() {
    _creator._removeTicker(this);
    super.dispose();
  }
}