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