1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/scheduler.dart';
7
8import 'framework.dart';
9
10export 'package:flutter/scheduler.dart' show TickerProvider;
11
12// Examples can assume:
13// late BuildContext context;
14
15/// Enables or disables tickers (and thus animation controllers) in the widget
16/// subtree.
17///
18/// This only works if [AnimationController] objects are created using
19/// widget-aware ticker providers. For example, using a
20/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
21class TickerMode extends StatefulWidget {
22 /// Creates a widget that enables or disables tickers.
23 const TickerMode({
24 super.key,
25 required this.enabled,
26 required this.child,
27 });
28
29 /// The requested ticker mode for this subtree.
30 ///
31 /// The effective ticker mode of this subtree may differ from this value
32 /// if there is an ancestor [TickerMode] with this field set to false.
33 ///
34 /// If true and all ancestor [TickerMode]s are also enabled, then tickers in
35 /// this subtree will tick.
36 ///
37 /// If false, then tickers in this subtree will not tick regardless of any
38 /// ancestor [TickerMode]s. Animations driven by such tickers are not paused,
39 /// they just don't call their callbacks. Time still elapses.
40 final bool enabled;
41
42 /// The widget below this widget in the tree.
43 ///
44 /// {@macro flutter.widgets.ProxyWidget.child}
45 final Widget child;
46
47 /// Whether tickers in the given subtree should be enabled or disabled.
48 ///
49 /// This is used automatically by [TickerProviderStateMixin] and
50 /// [SingleTickerProviderStateMixin] to decide if their tickers should be
51 /// enabled or disabled.
52 ///
53 /// In the absence of a [TickerMode] widget, this function defaults to true.
54 ///
55 /// Typical usage is as follows:
56 ///
57 /// ```dart
58 /// bool tickingEnabled = TickerMode.of(context);
59 /// ```
60 static bool of(BuildContext context) {
61 final _EffectiveTickerMode? widget = context.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
62 return widget?.enabled ?? true;
63 }
64
65 /// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`,
66 /// which indicates whether tickers are enabled in the given subtree.
67 ///
68 /// When that [TickerMode] enabled or disabled tickers, the listenable notifies
69 /// its listeners.
70 ///
71 /// While the [ValueListenable] is stable for the lifetime of the surrounding
72 /// [TickerMode], calling this method does not establish a dependency between
73 /// the `context` and the [TickerMode] and the widget owning the `context`
74 /// does not rebuild when the ticker mode changes from true to false or vice
75 /// versa. This is preferable when the ticker mode does not impact what is
76 /// currently rendered on screen, e.g. because it is only used to mute/unmute a
77 /// [Ticker]. Since no dependency is established, the widget owning the
78 /// `context` is also not informed when it is moved to a new location in the
79 /// tree where it may have a different [TickerMode] ancestor. When this
80 /// happens, the widget must manually unsubscribe from the old listenable,
81 /// obtain a new one from the new ancestor [TickerMode] by calling this method
82 /// again, and re-subscribe to it. [StatefulWidget]s can, for example, do this
83 /// in [State.activate], which is called after the widget has been moved to
84 /// a new location.
85 ///
86 /// Alternatively, [of] can be used instead of this method to create a
87 /// dependency between the provided `context` and the ancestor [TickerMode].
88 /// In this case, the widget automatically rebuilds when the ticker mode
89 /// changes or when it is moved to a new [TickerMode] ancestor, which
90 /// simplifies the management cost in the widget at the expensive of some
91 /// potential unnecessary rebuilds.
92 ///
93 /// In the absence of a [TickerMode] widget, this function returns a
94 /// [ValueListenable], whose [ValueListenable.value] is always true.
95 static ValueListenable<bool> getNotifier(BuildContext context) {
96 final _EffectiveTickerMode? widget = context.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
97 return widget?.notifier ?? const _ConstantValueListenable<bool>(true);
98 }
99
100 @override
101 State<TickerMode> createState() => _TickerModeState();
102}
103
104class _TickerModeState extends State<TickerMode> {
105 bool _ancestorTicketMode = true;
106 final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(true);
107
108 @override
109 void didChangeDependencies() {
110 super.didChangeDependencies();
111 _ancestorTicketMode = TickerMode.of(context);
112 _updateEffectiveMode();
113 }
114
115 @override
116 void didUpdateWidget(TickerMode oldWidget) {
117 super.didUpdateWidget(oldWidget);
118 _updateEffectiveMode();
119 }
120
121 @override
122 void dispose() {
123 _effectiveMode.dispose();
124 super.dispose();
125 }
126
127 void _updateEffectiveMode() {
128 _effectiveMode.value = _ancestorTicketMode && widget.enabled;
129 }
130
131 @override
132 Widget build(BuildContext context) {
133 return _EffectiveTickerMode(
134 enabled: _effectiveMode.value,
135 notifier: _effectiveMode,
136 child: widget.child,
137 );
138 }
139
140 @override
141 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
142 super.debugFillProperties(properties);
143 properties.add(FlagProperty('requested mode', value: widget.enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
144 }
145}
146
147class _EffectiveTickerMode extends InheritedWidget {
148 const _EffectiveTickerMode({
149 required this.enabled,
150 required this.notifier,
151 required super.child,
152 });
153
154 final bool enabled;
155 final ValueNotifier<bool> notifier;
156
157 @override
158 bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
159
160 @override
161 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
162 super.debugFillProperties(properties);
163 properties.add(FlagProperty('effective mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
164 }
165}
166
167/// Provides a single [Ticker] that is configured to only tick while the current
168/// tree is enabled, as defined by [TickerMode].
169///
170/// To create the [AnimationController] in a [State] that only uses a single
171/// [AnimationController], mix in this class, then pass `vsync: this`
172/// to the animation controller constructor.
173///
174/// This mixin only supports vending a single ticker. If you might have multiple
175/// [AnimationController] objects over the lifetime of the [State], use a full
176/// [TickerProviderStateMixin] instead.
177@optionalTypeArgs
178mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
179 Ticker? _ticker;
180
181 @override
182 Ticker createTicker(TickerCallback onTick) {
183 assert(() {
184 if (_ticker == null) {
185 return true;
186 }
187 throw FlutterError.fromParts(<DiagnosticsNode>[
188 ErrorSummary('$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
189 ErrorDescription('A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
190 ErrorHint(
191 'If a State is used for multiple AnimationController objects, or if it is passed to other '
192 'objects and those objects might use it more than one time in total, then instead of '
193 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
194 ),
195 ]);
196 }());
197 _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
198 _updateTickerModeNotifier();
199 _updateTicker(); // Sets _ticker.mute correctly.
200 return _ticker!;
201 }
202
203 @override
204 void dispose() {
205 assert(() {
206 if (_ticker == null || !_ticker!.isActive) {
207 return true;
208 }
209 throw FlutterError.fromParts(<DiagnosticsNode>[
210 ErrorSummary('$this was disposed with an active Ticker.'),
211 ErrorDescription(
212 '$runtimeType created a Ticker via its SingleTickerProviderStateMixin, but at the time '
213 'dispose() was called on the mixin, that Ticker was still active. The Ticker must '
214 'be disposed before calling super.dispose().',
215 ),
216 ErrorHint(
217 'Tickers used by AnimationControllers '
218 'should be disposed by calling dispose() on the AnimationController itself. '
219 'Otherwise, the ticker will leak.',
220 ),
221 _ticker!.describeForError('The offending ticker was'),
222 ]);
223 }());
224 _tickerModeNotifier?.removeListener(_updateTicker);
225 _tickerModeNotifier = null;
226 super.dispose();
227 }
228
229 ValueListenable<bool>? _tickerModeNotifier;
230
231 @override
232 void activate() {
233 super.activate();
234 // We may have a new TickerMode ancestor.
235 _updateTickerModeNotifier();
236 _updateTicker();
237 }
238
239 void _updateTicker() {
240 if (_ticker != null) {
241 _ticker!.muted = !_tickerModeNotifier!.value;
242 }
243 }
244
245 void _updateTickerModeNotifier() {
246 final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
247 if (newNotifier == _tickerModeNotifier) {
248 return;
249 }
250 _tickerModeNotifier?.removeListener(_updateTicker);
251 newNotifier.addListener(_updateTicker);
252 _tickerModeNotifier = newNotifier;
253 }
254
255 @override
256 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
257 super.debugFillProperties(properties);
258 String? tickerDescription;
259 if (_ticker != null) {
260 if (_ticker!.isActive && _ticker!.muted) {
261 tickerDescription = 'active but muted';
262 } else if (_ticker!.isActive) {
263 tickerDescription = 'active';
264 } else if (_ticker!.muted) {
265 tickerDescription = 'inactive and muted';
266 } else {
267 tickerDescription = 'inactive';
268 }
269 }
270 properties.add(DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null));
271 }
272}
273
274/// Provides [Ticker] objects that are configured to only tick while the current
275/// tree is enabled, as defined by [TickerMode].
276///
277/// To create an [AnimationController] in a class that uses this mixin, pass
278/// `vsync: this` to the animation controller constructor whenever you
279/// create a new animation controller.
280///
281/// If you only have a single [Ticker] (for example only a single
282/// [AnimationController]) for the lifetime of your [State], then using a
283/// [SingleTickerProviderStateMixin] is more efficient. This is the common case.
284@optionalTypeArgs
285mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
286 Set<Ticker>? _tickers;
287
288 @override
289 Ticker createTicker(TickerCallback onTick) {
290 if (_tickerModeNotifier == null) {
291 // Setup TickerMode notifier before we vend the first ticker.
292 _updateTickerModeNotifier();
293 }
294 assert(_tickerModeNotifier != null);
295 _tickers ??= <_WidgetTicker>{};
296 final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null)
297 ..muted = !_tickerModeNotifier!.value;
298 _tickers!.add(result);
299 return result;
300 }
301
302 void _removeTicker(_WidgetTicker ticker) {
303 assert(_tickers != null);
304 assert(_tickers!.contains(ticker));
305 _tickers!.remove(ticker);
306 }
307
308 ValueListenable<bool>? _tickerModeNotifier;
309
310 @override
311 void activate() {
312 super.activate();
313 // We may have a new TickerMode ancestor, get its Notifier.
314 _updateTickerModeNotifier();
315 _updateTickers();
316 }
317
318 void _updateTickers() {
319 if (_tickers != null) {
320 final bool muted = !_tickerModeNotifier!.value;
321 for (final Ticker ticker in _tickers!) {
322 ticker.muted = muted;
323 }
324 }
325 }
326
327 void _updateTickerModeNotifier() {
328 final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
329 if (newNotifier == _tickerModeNotifier) {
330 return;
331 }
332 _tickerModeNotifier?.removeListener(_updateTickers);
333 newNotifier.addListener(_updateTickers);
334 _tickerModeNotifier = newNotifier;
335 }
336
337 @override
338 void dispose() {
339 assert(() {
340 if (_tickers != null) {
341 for (final Ticker ticker in _tickers!) {
342 if (ticker.isActive) {
343 throw FlutterError.fromParts(<DiagnosticsNode>[
344 ErrorSummary('$this was disposed with an active Ticker.'),
345 ErrorDescription(
346 '$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
347 'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
348 'be disposed before calling super.dispose().',
349 ),
350 ErrorHint(
351 'Tickers used by AnimationControllers '
352 'should be disposed by calling dispose() on the AnimationController itself. '
353 'Otherwise, the ticker will leak.',
354 ),
355 ticker.describeForError('The offending ticker was'),
356 ]);
357 }
358 }
359 }
360 return true;
361 }());
362 _tickerModeNotifier?.removeListener(_updateTickers);
363 _tickerModeNotifier = null;
364 super.dispose();
365 }
366
367 @override
368 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
369 super.debugFillProperties(properties);
370 properties.add(DiagnosticsProperty<Set<Ticker>>(
371 'tickers',
372 _tickers,
373 description: _tickers != null ?
374 'tracking ${_tickers!.length} ticker${_tickers!.length == 1 ? "" : "s"}' :
375 null,
376 defaultValue: null,
377 ));
378 }
379}
380
381// This class should really be called _DisposingTicker or some such, but this
382// class name leaks into stack traces and error messages and that name would be
383// confusing. Instead we use the less precise but more anodyne "_WidgetTicker",
384// which attracts less attention.
385class _WidgetTicker extends Ticker {
386 _WidgetTicker(super.onTick, this._creator, { super.debugLabel });
387
388 final TickerProviderStateMixin _creator;
389
390 @override
391 void dispose() {
392 _creator._removeTicker(this);
393 super.dispose();
394 }
395}
396
397class _ConstantValueListenable<T> implements ValueListenable<T> {
398 const _ConstantValueListenable(this.value);
399
400 @override
401 void addListener(VoidCallback listener) {
402 // Intentionally left empty: Value cannot change, so we never have to
403 // notify registered listeners.
404 }
405
406 @override
407 void removeListener(VoidCallback listener) {
408 // Intentionally left empty: Value cannot change, so we never have to
409 // notify registered listeners.
410 }
411
412 @override
413 final T value;
414}
415