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';
6
7import 'animation.dart';
8
9export 'dart:ui' show VoidCallback;
10
11export 'animation.dart' show AnimationStatus, AnimationStatusListener;
12
13/// A mixin that helps listen to another object only when this object has registered listeners.
14///
15/// This mixin provides implementations of [didRegisterListener] and [didUnregisterListener],
16/// and therefore can be used in conjunction with mixins that require these methods,
17/// [AnimationLocalListenersMixin] and [AnimationLocalStatusListenersMixin].
18mixin AnimationLazyListenerMixin {
19 int _listenerCounter = 0;
20
21 /// Calls [didStartListening] every time a registration of a listener causes
22 /// an empty list of listeners to become non-empty.
23 ///
24 /// See also:
25 ///
26 /// * [didUnregisterListener], which may cause the listener list to
27 /// become empty again, and in turn cause this method to call
28 /// [didStartListening] again.
29 @protected
30 void didRegisterListener() {
31 assert(_listenerCounter >= 0);
32 if (_listenerCounter == 0) {
33 didStartListening();
34 }
35 _listenerCounter += 1;
36 }
37
38 /// Calls [didStopListening] when an only remaining listener is unregistered,
39 /// thus making the list empty.
40 ///
41 /// See also:
42 ///
43 /// * [didRegisterListener], which causes the listener list to become non-empty.
44 @protected
45 void didUnregisterListener() {
46 assert(_listenerCounter >= 1);
47 _listenerCounter -= 1;
48 if (_listenerCounter == 0) {
49 didStopListening();
50 }
51 }
52
53 /// Called when the number of listeners changes from zero to one.
54 @protected
55 void didStartListening();
56
57 /// Called when the number of listeners changes from one to zero.
58 @protected
59 void didStopListening();
60
61 /// Whether there are any listeners.
62 bool get isListening => _listenerCounter > 0;
63}
64
65/// A mixin that replaces the [didRegisterListener]/[didUnregisterListener] contract
66/// with a dispose contract.
67///
68/// This mixin provides implementations of [didRegisterListener] and [didUnregisterListener],
69/// and therefore can be used in conjunction with mixins that require these methods,
70/// [AnimationLocalListenersMixin] and [AnimationLocalStatusListenersMixin].
71mixin AnimationEagerListenerMixin {
72 /// This implementation ignores listener registrations.
73 @protected
74 void didRegisterListener() {}
75
76 /// This implementation ignores listener registrations.
77 @protected
78 void didUnregisterListener() {}
79
80 /// Release the resources used by this object. The object is no longer usable
81 /// after this method is called.
82 @mustCallSuper
83 void dispose() {}
84}
85
86/// A mixin that implements the [addListener]/[removeListener] protocol and notifies
87/// all the registered listeners when [notifyListeners] is called.
88///
89/// This mixin requires that the mixing class provide methods [didRegisterListener]
90/// and [didUnregisterListener]. Implementations of these methods can be obtained
91/// by mixing in another mixin from this library, such as [AnimationLazyListenerMixin].
92mixin AnimationLocalListenersMixin {
93 final HashedObserverList<VoidCallback> _listeners = HashedObserverList<VoidCallback>();
94
95 /// Called immediately before a listener is added via [addListener].
96 ///
97 /// At the time this method is called the registered listener is not yet
98 /// notified by [notifyListeners].
99 @protected
100 void didRegisterListener();
101
102 /// Called immediately after a listener is removed via [removeListener].
103 ///
104 /// At the time this method is called the removed listener is no longer
105 /// notified by [notifyListeners].
106 @protected
107 void didUnregisterListener();
108
109 /// Calls the listener every time the value of the animation changes.
110 ///
111 /// Listeners can be removed with [removeListener].
112 void addListener(VoidCallback listener) {
113 didRegisterListener();
114 _listeners.add(listener);
115 }
116
117 /// Stop calling the listener every time the value of the animation changes.
118 ///
119 /// Listeners can be added with [addListener].
120 void removeListener(VoidCallback listener) {
121 final bool removed = _listeners.remove(listener);
122 if (removed) {
123 didUnregisterListener();
124 }
125 }
126
127 /// Removes all listeners added with [addListener].
128 ///
129 /// This method is typically called from the `dispose` method of the class
130 /// using this mixin if the class also uses the [AnimationEagerListenerMixin].
131 ///
132 /// Calling this method will not trigger [didUnregisterListener].
133 @protected
134 void clearListeners() {
135 _listeners.clear();
136 }
137
138 /// Calls all the listeners.
139 ///
140 /// If listeners are added or removed during this function, the modifications
141 /// will not change which listeners are called during this iteration.
142 @protected
143 @pragma('vm:notify-debugger-on-exception')
144 void notifyListeners() {
145 final List<VoidCallback> localListeners = _listeners.toList(growable: false);
146 for (final VoidCallback listener in localListeners) {
147 InformationCollector? collector;
148 assert(() {
149 collector =
150 () => <DiagnosticsNode>[
151 DiagnosticsProperty<AnimationLocalListenersMixin>(
152 'The $runtimeType notifying listeners was',
153 this,
154 style: DiagnosticsTreeStyle.errorProperty,
155 ),
156 ];
157 return true;
158 }());
159 try {
160 if (_listeners.contains(listener)) {
161 listener();
162 }
163 } catch (exception, stack) {
164 FlutterError.reportError(
165 FlutterErrorDetails(
166 exception: exception,
167 stack: stack,
168 library: 'animation library',
169 context: ErrorDescription('while notifying listeners for $runtimeType'),
170 informationCollector: collector,
171 ),
172 );
173 }
174 }
175 }
176}
177
178/// A mixin that implements the addStatusListener/removeStatusListener protocol
179/// and notifies all the registered listeners when notifyStatusListeners is
180/// called.
181///
182/// This mixin requires that the mixing class provide methods [didRegisterListener]
183/// and [didUnregisterListener]. Implementations of these methods can be obtained
184/// by mixing in another mixin from this library, such as [AnimationLazyListenerMixin].
185mixin AnimationLocalStatusListenersMixin {
186 final ObserverList<AnimationStatusListener> _statusListeners =
187 ObserverList<AnimationStatusListener>();
188
189 /// Called immediately before a status listener is added via [addStatusListener].
190 ///
191 /// At the time this method is called the registered listener is not yet
192 /// notified by [notifyStatusListeners].
193 @protected
194 void didRegisterListener();
195
196 /// Called immediately after a status listener is removed via [removeStatusListener].
197 ///
198 /// At the time this method is called the removed listener is no longer
199 /// notified by [notifyStatusListeners].
200 @protected
201 void didUnregisterListener();
202
203 /// Calls listener every time the status of the animation changes.
204 ///
205 /// Listeners can be removed with [removeStatusListener].
206 void addStatusListener(AnimationStatusListener listener) {
207 didRegisterListener();
208 _statusListeners.add(listener);
209 }
210
211 /// Stops calling the listener every time the status of the animation changes.
212 ///
213 /// Listeners can be added with [addStatusListener].
214 void removeStatusListener(AnimationStatusListener listener) {
215 final bool removed = _statusListeners.remove(listener);
216 if (removed) {
217 didUnregisterListener();
218 }
219 }
220
221 /// Removes all listeners added with [addStatusListener].
222 ///
223 /// This method is typically called from the `dispose` method of the class
224 /// using this mixin if the class also uses the [AnimationEagerListenerMixin].
225 ///
226 /// Calling this method will not trigger [didUnregisterListener].
227 @protected
228 void clearStatusListeners() {
229 _statusListeners.clear();
230 }
231
232 /// Calls all the status listeners.
233 ///
234 /// If listeners are added or removed during this function, the modifications
235 /// will not change which listeners are called during this iteration.
236 @protected
237 @pragma('vm:notify-debugger-on-exception')
238 void notifyStatusListeners(AnimationStatus status) {
239 final List<AnimationStatusListener> localListeners = _statusListeners.toList(growable: false);
240 for (final AnimationStatusListener listener in localListeners) {
241 try {
242 if (_statusListeners.contains(listener)) {
243 listener(status);
244 }
245 } catch (exception, stack) {
246 InformationCollector? collector;
247 assert(() {
248 collector =
249 () => <DiagnosticsNode>[
250 DiagnosticsProperty<AnimationLocalStatusListenersMixin>(
251 'The $runtimeType notifying status listeners was',
252 this,
253 style: DiagnosticsTreeStyle.errorProperty,
254 ),
255 ];
256 return true;
257 }());
258 FlutterError.reportError(
259 FlutterErrorDetails(
260 exception: exception,
261 stack: stack,
262 library: 'animation library',
263 context: ErrorDescription('while notifying status listeners for $runtimeType'),
264 informationCollector: collector,
265 ),
266 );
267 }
268 }
269 }
270}
271