ticker_provider.dart 8.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// 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/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].
class TickerMode extends InheritedWidget {
  /// 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,
    Widget child
26 27
  }) : assert(enabled != null),
       super(key: key, child: child);
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

  /// The current ticker mode of this subtree.
  ///
  /// If true, then tickers in this subtree will tick.
  ///
  /// If false, then tickers in this subtree will not tick. Animations driven by
  /// such tickers are not paused, they just don't call their callbacks. Time
  /// still elapses.
  final bool enabled;

  /// 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.
45 46 47 48 49 50
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// bool tickingEnabled = TickerMode.of(context);
  /// ```
51
  static bool of(BuildContext context) {
52
    final TickerMode widget = context.inheritFromWidgetOfExactType(TickerMode);
53 54 55 56
    return widget?.enabled ?? true;
  }

  @override
57
  bool updateShouldNotify(TickerMode oldWidget) => enabled != oldWidget.enabled;
58 59

  @override
60 61 62
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new FlagProperty('mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
63 64 65 66 67 68 69 70 71 72 73 74 75
  }
}

/// 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.
76 77
@optionalTypeArgs
abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends State<T> implements TickerProvider {
78 79 80
  // This class is intended to be used as a mixin, and should not be
  // extended directly.
  factory SingleTickerProviderStateMixin._() => null;
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

  Ticker _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    assert(() {
      if (_ticker == null)
        return true;
      throw new FlutterError(
        '$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.\n'
        'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. 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.'
      );
96
    }());
97
    _ticker = new Ticker(onTick, debugLabel: 'created by $this');
98 99 100 101
    // 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.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
    return _ticker;
  }

  @override
  void dispose() {
    assert(() {
      if (_ticker == null || !_ticker.isActive)
        return true;
      throw new FlutterError(
        '$this was disposed with an active Ticker.\n'
        '$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(). Tickers used by AnimationControllers '
        'should be disposed by calling dispose() on the AnimationController itself. '
        'Otherwise, the ticker will leak.\n'
        'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}'
      );
119
    }());
120 121 122 123
    super.dispose();
  }

  @override
124
  void didChangeDependencies() {
125 126
    if (_ticker != null)
      _ticker.muted = !TickerMode.of(context);
127
    super.didChangeDependencies();
128 129 130
  }

  @override
131 132
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
133
    String tickerDescription;
134 135
    if (_ticker != null) {
      if (_ticker.isActive && _ticker.muted)
136
        tickerDescription = 'active but muted';
137
      else if (_ticker.isActive)
138
        tickerDescription = 'active';
139
      else if (_ticker.muted)
140
        tickerDescription = 'inactive and muted';
141
      else
142
        tickerDescription = 'inactive';
143
    }
144
    properties.add(new DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null));
145 146 147 148 149 150 151 152 153 154 155 156 157
  }
}

/// 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.
158 159
@optionalTypeArgs
abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State<T> implements TickerProvider {
160 161 162
  // This class is intended to be used as a mixin, and should not be
  // extended directly.
  factory TickerProviderStateMixin._() => null;
163 164 165 166 167 168

  Set<Ticker> _tickers;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _tickers ??= new Set<_WidgetTicker>();
169
    final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this');
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    _tickers.add(result);
    return result;
  }

  void _removeTicker(_WidgetTicker ticker) {
    assert(_tickers != null);
    assert(_tickers.contains(ticker));
    _tickers.remove(ticker);
  }

  @override
  void dispose() {
    assert(() {
      if (_tickers != null) {
        for (Ticker ticker in _tickers) {
          if (ticker.isActive) {
            throw new FlutterError(
              '$this was disposed with an active Ticker.\n'
              '$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(). Tickers used by AnimationControllers '
              'should be disposed by calling dispose() on the AnimationController itself. '
              'Otherwise, the ticker will leak.\n'
              'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}'
            );
          }
        }
      }
      return true;
199
    }());
200 201 202 203
    super.dispose();
  }

  @override
204
  void didChangeDependencies() {
205 206
    final bool muted = !TickerMode.of(context);
    if (_tickers != null) {
207
      for (Ticker ticker in _tickers) {
208
        ticker.muted = muted;
209
      }
210
    }
211
    super.didChangeDependencies();
212 213 214
  }

  @override
215 216 217
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new DiagnosticsProperty<Set<Ticker>>(
218 219 220 221 222 223 224
      'tickers',
      _tickers,
      description: _tickers != null ?
        'tracking ${_tickers.length} ticker${_tickers.length == 1 ? "" : "s"}' :
        null,
      defaultValue: null,
    ));
225 226 227 228 229 230 231 232 233 234
  }
}

// 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 {
  _WidgetTicker(TickerCallback onTick, this._creator, { String debugLabel }) : super(onTick, debugLabel: debugLabel);

235
  final TickerProviderStateMixin _creator;
236 237 238 239 240 241 242

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