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