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