| // Copyright 2014 The Flutter 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; |
| |
| // Examples can assume: |
| // late BuildContext context; |
| |
| /// 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 StatefulWidget { |
| /// Creates a widget that enables or disables tickers. |
| /// |
| /// The [enabled] argument must not be null. |
| const TickerMode({ |
| super.key, |
| required this.enabled, |
| required this.child, |
| }) : assert(enabled != null); |
| |
| /// 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. |
| /// |
| /// If true and all ancestor [TickerMode]s are also enabled, then tickers in |
| /// this subtree will tick. |
| /// |
| /// 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. |
| final bool enabled; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.child} |
| final Widget child; |
| |
| /// 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. |
| /// |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// bool tickingEnabled = TickerMode.of(context); |
| /// ``` |
| static bool of(BuildContext context) { |
| final _EffectiveTickerMode? widget = context.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>(); |
| return widget?.enabled ?? true; |
| } |
| |
| /// Obtains a [ValueNotifier] from the [TickerMode] surrounding the `context`, |
| /// which indicates whether tickers are enabled in the given subtree. |
| /// |
| /// When that [TickerMode] enabled or disabled tickers, the notifier notifies |
| /// its listeners. |
| /// |
| /// While the [ValueNotifier] is stable for the lifetime of the surrounding |
| /// [TickerMode], calling this method does not establish a dependency between |
| /// the `context` and the [TickerMode] and the widget owning the `context` |
| /// does not rebuild when the ticker mode changes from true to false or vice |
| /// versa. This is preferable when the ticker mode does not impact what is |
| /// currently rendered on screen, e.g. because it is only used to mute/unmute a |
| /// [Ticker]. Since no dependency is established, the widget owning the |
| /// `context` is also not informed when it is moved to a new location in the |
| /// tree where it may have a different [TickerMode] ancestor. When this |
| /// happens, the widget must manually unsubscribe from the old notifier, |
| /// obtain a new one from the new ancestor [TickerMode] by calling this method |
| /// again, and re-subscribe to it. [StatefulWidget]s can, for example, do this |
| /// in [State.activate], which is called after the widget has been moved to |
| /// a new location. |
| /// |
| /// Alternatively, [of] can be used instead of this method to create a |
| /// dependency between the provided `context` and the ancestor [TickerMode]. |
| /// In this case, the widget automatically rebuilds when the ticker mode |
| /// changes or when it is moved to a new [TickerMode] ancestor, which |
| /// simplifies the management cost in the widget at the expensive of some |
| /// potential unnecessary rebuilds. |
| /// |
| /// In the absence of a [TickerMode] widget, this function returns a |
| /// [ValueNotifier], whose [ValueNotifier.value] is always true. |
| static ValueNotifier<bool> getNotifier(BuildContext context) { |
| final _EffectiveTickerMode? widget = context.getElementForInheritedWidgetOfExactType<_EffectiveTickerMode>()?.widget as _EffectiveTickerMode?; |
| return widget?.notifier ?? ValueNotifier<bool>(true); |
| } |
| |
| @override |
| State<TickerMode> createState() => _TickerModeState(); |
| } |
| |
| class _TickerModeState extends State<TickerMode> { |
| bool _ancestorTicketMode = true; |
| final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(true); |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| _ancestorTicketMode = TickerMode.of(context); |
| _updateEffectiveMode(); |
| } |
| |
| @override |
| void didUpdateWidget(TickerMode oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| _updateEffectiveMode(); |
| } |
| |
| @override |
| void dispose() { |
| _effectiveMode.dispose(); |
| super.dispose(); |
| } |
| |
| void _updateEffectiveMode() { |
| _effectiveMode.value = _ancestorTicketMode && widget.enabled; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return _EffectiveTickerMode( |
| enabled: _effectiveMode.value, |
| notifier: _effectiveMode, |
| child: widget.child, |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(FlagProperty('requested mode', value: widget.enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true)); |
| } |
| } |
| |
| class _EffectiveTickerMode extends InheritedWidget { |
| const _EffectiveTickerMode({ |
| required this.enabled, |
| required this.notifier, |
| required super.child, |
| }) : assert(enabled != null); |
| |
| final bool enabled; |
| final ValueNotifier<bool> notifier; |
| |
| @override |
| bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(FlagProperty('effective mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true)); |
| } |
| } |
| |
| /// 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. |
| @optionalTypeArgs |
| mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider { |
| Ticker? _ticker; |
| |
| @override |
| Ticker createTicker(TickerCallback onTick) { |
| assert(() { |
| if (_ticker == null) { |
| return true; |
| } |
| 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.', |
| ), |
| ]); |
| }()); |
| _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null); |
| _updateTickerModeNotifier(); |
| _updateTicker(); // Sets _ticker.mute correctly. |
| return _ticker!; |
| } |
| |
| @override |
| void dispose() { |
| assert(() { |
| if (_ticker == null || !_ticker!.isActive) { |
| return true; |
| } |
| 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.', |
| ), |
| _ticker!.describeForError('The offending ticker was'), |
| ]); |
| }()); |
| _tickerModeNotifier?.removeListener(_updateTicker); |
| _tickerModeNotifier = null; |
| super.dispose(); |
| } |
| |
| ValueNotifier<bool>? _tickerModeNotifier; |
| |
| @override |
| void activate() { |
| super.activate(); |
| // We may have a new TickerMode ancestor. |
| _updateTickerModeNotifier(); |
| _updateTicker(); |
| } |
| |
| void _updateTicker() { |
| if (_ticker != null) { |
| _ticker!.muted = !_tickerModeNotifier!.value; |
| } |
| } |
| |
| void _updateTickerModeNotifier() { |
| final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context); |
| if (newNotifier == _tickerModeNotifier) { |
| return; |
| } |
| _tickerModeNotifier?.removeListener(_updateTicker); |
| newNotifier.addListener(_updateTicker); |
| _tickerModeNotifier = newNotifier; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| String? tickerDescription; |
| if (_ticker != null) { |
| if (_ticker!.isActive && _ticker!.muted) { |
| tickerDescription = 'active but muted'; |
| } else if (_ticker!.isActive) { |
| tickerDescription = 'active'; |
| } else if (_ticker!.muted) { |
| tickerDescription = 'inactive and muted'; |
| } else { |
| tickerDescription = 'inactive'; |
| } |
| } |
| properties.add(DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null)); |
| } |
| } |
| |
| /// 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. |
| @optionalTypeArgs |
| mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider { |
| Set<Ticker>? _tickers; |
| |
| @override |
| Ticker createTicker(TickerCallback onTick) { |
| if (_tickerModeNotifier == null) { |
| // Setup TickerMode notifier before we vend the first ticker. |
| _updateTickerModeNotifier(); |
| } |
| assert(_tickerModeNotifier != null); |
| _tickers ??= <_WidgetTicker>{}; |
| final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null) |
| ..muted = !_tickerModeNotifier!.value; |
| _tickers!.add(result); |
| return result; |
| } |
| |
| void _removeTicker(_WidgetTicker ticker) { |
| assert(_tickers != null); |
| assert(_tickers!.contains(ticker)); |
| _tickers!.remove(ticker); |
| } |
| |
| ValueNotifier<bool>? _tickerModeNotifier; |
| |
| @override |
| void activate() { |
| super.activate(); |
| // We may have a new TickerMode ancestor, get its Notifier. |
| _updateTickerModeNotifier(); |
| _updateTickers(); |
| } |
| |
| void _updateTickers() { |
| if (_tickers != null) { |
| final bool muted = !_tickerModeNotifier!.value; |
| for (final Ticker ticker in _tickers!) { |
| ticker.muted = muted; |
| } |
| } |
| } |
| |
| void _updateTickerModeNotifier() { |
| final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context); |
| if (newNotifier == _tickerModeNotifier) { |
| return; |
| } |
| _tickerModeNotifier?.removeListener(_updateTickers); |
| newNotifier.addListener(_updateTickers); |
| _tickerModeNotifier = newNotifier; |
| } |
| |
| @override |
| void dispose() { |
| assert(() { |
| if (_tickers != null) { |
| for (final Ticker ticker in _tickers!) { |
| if (ticker.isActive) { |
| 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'), |
| ]); |
| } |
| } |
| } |
| return true; |
| }()); |
| _tickerModeNotifier?.removeListener(_updateTickers); |
| _tickerModeNotifier = null; |
| super.dispose(); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Set<Ticker>>( |
| 'tickers', |
| _tickers, |
| description: _tickers != null ? |
| 'tracking ${_tickers!.length} ticker${_tickers!.length == 1 ? "" : "s"}' : |
| null, |
| defaultValue: null, |
| )); |
| } |
| } |
| |
| // 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(super.onTick, this._creator, { super.debugLabel }); |
| |
| final TickerProviderStateMixin _creator; |
| |
| @override |
| void dispose() { |
| _creator._removeTicker(this); |
| super.dispose(); |
| } |
| } |