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/widgets.dart';
6///
7/// @docImport 'semantics.dart';
8library;
9
10import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
11
12import 'package:flutter/foundation.dart';
13import 'package:flutter/scheduler.dart';
14import 'package:flutter/services.dart';
15
16import 'debug.dart';
17
18export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
19
20/// The glue between the semantics layer and the Flutter engine.
21mixin SemanticsBinding on BindingBase {
22 @override
23 void initInstances() {
24 super.initInstances();
25 _instance = this;
26 _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
27 platformDispatcher
28 ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
29 ..onSemanticsActionEvent = _handleSemanticsActionEvent
30 ..onAccessibilityFeaturesChanged = () {
31 // TODO(chunhtai): Web should not notify accessibility feature changes during updateSemantics
32 // https://github.com/flutter/flutter/issues/158399
33 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
34 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
35 handleAccessibilityFeaturesChanged();
36 }, debugLabel: 'SemanticsBinding.handleAccessibilityFeaturesChanged');
37 } else {
38 handleAccessibilityFeaturesChanged();
39 }
40 };
41 _handleSemanticsEnabledChanged();
42 }
43
44 /// The current [SemanticsBinding], if one has been created.
45 ///
46 /// Provides access to the features exposed by this mixin. The binding must
47 /// be initialized before using this getter; this is typically done by calling
48 /// [runApp] or [WidgetsFlutterBinding.ensureInitialized].
49 static SemanticsBinding get instance => BindingBase.checkInstance(_instance);
50 static SemanticsBinding? _instance;
51
52 /// Whether semantics information must be collected.
53 ///
54 /// Returns true if either the platform has requested semantics information
55 /// to be generated or if [ensureSemantics] has been called otherwise.
56 ///
57 /// To get notified when this value changes register a listener with
58 /// [addSemanticsEnabledListener].
59 bool get semanticsEnabled {
60 assert(_semanticsEnabled.value == (_outstandingHandles > 0));
61 return _semanticsEnabled.value;
62 }
63
64 late final ValueNotifier<bool> _semanticsEnabled = ValueNotifier<bool>(
65 platformDispatcher.semanticsEnabled,
66 );
67
68 /// Adds a `listener` to be called when [semanticsEnabled] changes.
69 ///
70 /// See also:
71 ///
72 /// * [removeSemanticsEnabledListener] to remove the listener again.
73 /// * [ValueNotifier.addListener], which documents how and when listeners are
74 /// called.
75 void addSemanticsEnabledListener(VoidCallback listener) {
76 _semanticsEnabled.addListener(listener);
77 }
78
79 /// Removes a `listener` added by [addSemanticsEnabledListener].
80 ///
81 /// See also:
82 ///
83 /// * [ValueNotifier.removeListener], which documents how listeners are
84 /// removed.
85 void removeSemanticsEnabledListener(VoidCallback listener) {
86 _semanticsEnabled.removeListener(listener);
87 }
88
89 final ObserverList<ValueSetter<ui.SemanticsActionEvent>> _semanticsActionListeners =
90 ObserverList<ValueSetter<ui.SemanticsActionEvent>>();
91
92 /// Adds a listener that is called for every [ui.SemanticsActionEvent] received.
93 ///
94 /// The listeners are called before [performSemanticsAction] is invoked.
95 ///
96 /// To remove the listener, call [removeSemanticsActionListener].
97 void addSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
98 _semanticsActionListeners.add(listener);
99 }
100
101 /// Removes a listener previously added with [addSemanticsActionListener].
102 void removeSemanticsActionListener(ValueSetter<ui.SemanticsActionEvent> listener) {
103 _semanticsActionListeners.remove(listener);
104 }
105
106 /// The number of clients registered to listen for semantics.
107 ///
108 /// The number is increased whenever [ensureSemantics] is called and decreased
109 /// when [SemanticsHandle.dispose] is called.
110 int get debugOutstandingSemanticsHandles => _outstandingHandles;
111 int _outstandingHandles = 0;
112
113 /// Creates a new [SemanticsHandle] and requests the collection of semantics
114 /// information.
115 ///
116 /// Semantics information are only collected when there are clients interested
117 /// in them. These clients express their interest by holding a
118 /// [SemanticsHandle].
119 ///
120 /// Clients can close their [SemanticsHandle] by calling
121 /// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects
122 /// are closed, semantics information are no longer collected.
123 SemanticsHandle ensureSemantics() {
124 assert(_outstandingHandles >= 0);
125 _outstandingHandles++;
126 assert(_outstandingHandles > 0);
127 _semanticsEnabled.value = true;
128 return SemanticsHandle._(_didDisposeSemanticsHandle);
129 }
130
131 void _didDisposeSemanticsHandle() {
132 assert(_outstandingHandles > 0);
133 _outstandingHandles--;
134 assert(_outstandingHandles >= 0);
135 _semanticsEnabled.value = _outstandingHandles > 0;
136 }
137
138 // Handle for semantics request from the platform.
139 SemanticsHandle? _semanticsHandle;
140
141 void _handleSemanticsEnabledChanged() {
142 if (platformDispatcher.semanticsEnabled) {
143 _semanticsHandle ??= ensureSemantics();
144 } else {
145 _semanticsHandle?.dispose();
146 _semanticsHandle = null;
147 }
148 }
149
150 void _handleSemanticsActionEvent(ui.SemanticsActionEvent action) {
151 final Object? arguments = action.arguments;
152 final ui.SemanticsActionEvent decodedAction = arguments is ByteData
153 ? action.copyWith(arguments: const StandardMessageCodec().decodeMessage(arguments))
154 : action;
155 // Listeners may get added/removed while the iteration is in progress. Since the list cannot
156 // be modified while iterating, we are creating a local copy for the iteration.
157 final List<ValueSetter<ui.SemanticsActionEvent>> localListeners = _semanticsActionListeners
158 .toList(growable: false);
159 for (final ValueSetter<ui.SemanticsActionEvent> listener in localListeners) {
160 if (_semanticsActionListeners.contains(listener)) {
161 listener(decodedAction);
162 }
163 }
164 performSemanticsAction(decodedAction);
165 }
166
167 /// Called whenever the platform requests an action to be performed on a
168 /// [SemanticsNode].
169 ///
170 /// This callback is invoked when a user interacts with the app via an
171 /// accessibility service (e.g. TalkBack and VoiceOver) and initiates an
172 /// action on the focused node.
173 ///
174 /// Bindings that mixin the [SemanticsBinding] must implement this method and
175 /// perform the given `action` on the [SemanticsNode] specified by
176 /// [ui.SemanticsActionEvent.nodeId].
177 ///
178 /// See [dart:ui.PlatformDispatcher.onSemanticsActionEvent].
179 @protected
180 void performSemanticsAction(ui.SemanticsActionEvent action);
181
182 /// The currently active set of [ui.AccessibilityFeatures].
183 ///
184 /// This is set when the binding is first initialized and updated whenever a
185 /// flag is changed.
186 ///
187 /// To listen to changes to accessibility features, create a
188 /// [WidgetsBindingObserver] and listen to
189 /// [WidgetsBindingObserver.didChangeAccessibilityFeatures].
190 ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures;
191 late ui.AccessibilityFeatures _accessibilityFeatures;
192
193 /// Called when the platform accessibility features change.
194 ///
195 /// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged].
196 @protected
197 @mustCallSuper
198 void handleAccessibilityFeaturesChanged() {
199 _accessibilityFeatures = platformDispatcher.accessibilityFeatures;
200 }
201
202 /// Creates an empty semantics update builder.
203 ///
204 /// The caller is responsible for filling out the semantics node updates.
205 ///
206 /// This method is used by the [SemanticsOwner] to create builder for all its
207 /// semantics updates.
208 ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
209 return ui.SemanticsUpdateBuilder();
210 }
211
212 /// The platform is requesting that animations be disabled or simplified.
213 ///
214 /// This setting can be overridden for testing or debugging by setting
215 /// [debugSemanticsDisableAnimations].
216 bool get disableAnimations {
217 bool value = _accessibilityFeatures.disableAnimations;
218 assert(() {
219 if (debugSemanticsDisableAnimations != null) {
220 value = debugSemanticsDisableAnimations!;
221 }
222 return true;
223 }());
224 return value;
225 }
226}
227
228/// A reference to the semantics information generated by the framework.
229///
230/// Semantics information are only collected when there are clients interested
231/// in them. These clients express their interest by holding a
232/// [SemanticsHandle]. When the client no longer needs the
233/// semantics information, it must call [dispose] on the [SemanticsHandle] to
234/// close it. When all open [SemanticsHandle]s are disposed, the framework will
235/// stop updating the semantics information.
236///
237/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics].
238class SemanticsHandle {
239 SemanticsHandle._(this._onDispose) {
240 assert(debugMaybeDispatchCreated('semantics', 'SemanticsHandle', this));
241 }
242
243 final VoidCallback _onDispose;
244
245 /// Closes the semantics handle.
246 ///
247 /// When all the outstanding [SemanticsHandle] objects are closed, the
248 /// framework will stop generating semantics information.
249 @mustCallSuper
250 void dispose() {
251 assert(debugMaybeDispatchDisposed(this));
252 _onDispose();
253 }
254}
255