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 | |
6 | import 'package:flutter/foundation.dart'; |
7 | import 'package:flutter/painting.dart'; |
8 | |
9 | export 'dart:ui' show TextDirection; |
10 | |
11 | /// Determines the assertiveness level of the accessibility announcement. |
12 | /// |
13 | /// It is used by [AnnounceSemanticsEvent] to determine the priority with which |
14 | /// assistive technology should treat announcements. |
15 | enum Assertiveness { |
16 | /// The assistive technology will speak changes whenever the user is idle. |
17 | polite, |
18 | |
19 | /// The assistive technology will interrupt any announcement that it is |
20 | /// currently making to notify the user about the change. |
21 | /// |
22 | /// It should only be used for time-sensitive/critical notifications. |
23 | assertive, |
24 | } |
25 | |
26 | /// An event sent by the application to notify interested listeners that |
27 | /// something happened to the user interface (e.g. a view scrolled). |
28 | /// |
29 | /// These events are usually interpreted by assistive technologies to give the |
30 | /// user additional clues about the current state of the UI. |
31 | abstract class SemanticsEvent { |
32 | /// Initializes internal fields. |
33 | /// |
34 | /// [type] is a string that identifies this class of [SemanticsEvent]s. |
35 | const SemanticsEvent(this.type); |
36 | |
37 | /// The type of this event. |
38 | /// |
39 | /// The type is used by the engine to translate this event into the |
40 | /// appropriate native event (`UIAccessibility*Notification` on iOS and |
41 | /// `AccessibilityEvent` on Android). |
42 | final String type; |
43 | |
44 | /// Converts this event to a Map that can be encoded with |
45 | /// [StandardMessageCodec]. |
46 | /// |
47 | /// [nodeId] is the unique identifier of the semantics node associated with |
48 | /// the event, or null if the event is not associated with a semantics node. |
49 | Map<String, dynamic> toMap({ int? nodeId }) { |
50 | final Map<String, dynamic> event = <String, dynamic>{ |
51 | 'type' : type, |
52 | 'data' : getDataMap(), |
53 | }; |
54 | if (nodeId != null) { |
55 | event['nodeId' ] = nodeId; |
56 | } |
57 | |
58 | return event; |
59 | } |
60 | |
61 | /// Returns the event's data object. |
62 | Map<String, dynamic> getDataMap(); |
63 | |
64 | @override |
65 | String toString() { |
66 | final List<String> pairs = <String>[]; |
67 | final Map<String, dynamic> dataMap = getDataMap(); |
68 | final List<String> sortedKeys = dataMap.keys.toList()..sort(); |
69 | for (final String key in sortedKeys) { |
70 | pairs.add(' $key: ${dataMap[key]}' ); |
71 | } |
72 | return ' ${objectRuntimeType(this, 'SemanticsEvent' )}( ${pairs.join(', ' )})' ; |
73 | } |
74 | } |
75 | |
76 | /// An event for a semantic announcement. |
77 | /// |
78 | /// This should be used for announcement that are not seamlessly announced by |
79 | /// the system as a result of a UI state change. |
80 | /// |
81 | /// For example a camera application can use this method to make accessibility |
82 | /// announcements regarding objects in the viewfinder. |
83 | /// |
84 | /// When possible, prefer using mechanisms like [Semantics] to implicitly |
85 | /// trigger announcements over using this event. |
86 | class AnnounceSemanticsEvent extends SemanticsEvent { |
87 | |
88 | /// Constructs an event that triggers an announcement by the platform. |
89 | const AnnounceSemanticsEvent(this.message, this.textDirection, {this.assertiveness = Assertiveness.polite}) |
90 | : super('announce' ); |
91 | |
92 | /// The message to announce. |
93 | final String message; |
94 | |
95 | /// Text direction for [message]. |
96 | final TextDirection textDirection; |
97 | |
98 | /// Determines whether the announcement should interrupt any existing announcement, |
99 | /// or queue after it. |
100 | /// |
101 | /// On the web this option uses the aria-live level to set the assertiveness |
102 | /// of the announcement. On iOS, Android, Windows, Linux, macOS, and Fuchsia |
103 | /// this option currently has no effect. |
104 | final Assertiveness assertiveness; |
105 | |
106 | @override |
107 | Map<String, dynamic> getDataMap() { |
108 | return <String, dynamic> { |
109 | 'message' : message, |
110 | 'textDirection' : textDirection.index, |
111 | if (assertiveness != Assertiveness.polite) |
112 | 'assertiveness' : assertiveness.index, |
113 | }; |
114 | } |
115 | } |
116 | |
117 | /// An event for a semantic announcement of a tooltip. |
118 | /// |
119 | /// This is only used by Android to announce tooltip values. |
120 | class TooltipSemanticsEvent extends SemanticsEvent { |
121 | /// Constructs an event that triggers a tooltip announcement by the platform. |
122 | const TooltipSemanticsEvent(this.message) : super('tooltip' ); |
123 | |
124 | /// The text content of the tooltip. |
125 | final String message; |
126 | |
127 | @override |
128 | Map<String, dynamic> getDataMap() { |
129 | return <String, dynamic>{ |
130 | 'message' : message, |
131 | }; |
132 | } |
133 | } |
134 | |
135 | /// An event which triggers long press semantic feedback. |
136 | /// |
137 | /// Currently only honored on Android. Triggers a long-press specific sound |
138 | /// when TalkBack is enabled. |
139 | class LongPressSemanticsEvent extends SemanticsEvent { |
140 | /// Constructs an event that triggers a long-press semantic feedback by the platform. |
141 | const LongPressSemanticsEvent() : super('longPress' ); |
142 | |
143 | @override |
144 | Map<String, dynamic> getDataMap() => const <String, dynamic>{}; |
145 | } |
146 | |
147 | /// An event which triggers tap semantic feedback. |
148 | /// |
149 | /// Currently only honored on Android. Triggers a tap specific sound when |
150 | /// TalkBack is enabled. |
151 | class TapSemanticEvent extends SemanticsEvent { |
152 | /// Constructs an event that triggers a long-press semantic feedback by the platform. |
153 | const TapSemanticEvent() : super('tap' ); |
154 | |
155 | @override |
156 | Map<String, dynamic> getDataMap() => const <String, dynamic>{}; |
157 | } |
158 | |
159 | /// An event to move the accessibility focus. |
160 | /// |
161 | /// Using this API is generally not recommended, as it may break a users' expectation of |
162 | /// how a11y focus works and therefore should be just very carefully. |
163 | /// |
164 | /// One possible use case: |
165 | /// For example, the currently focused rendering object is replaced by another rendering |
166 | /// object. In general, such design should be avoided if possible. If not, one may want |
167 | /// to refocus the newly added rendering object. |
168 | /// |
169 | /// One example that is not recommended: |
170 | /// When a new popup or dropdown opens, moving the focus in these cases may confuse users |
171 | /// and make it less accessible. |
172 | /// |
173 | /// {@tool snippet} |
174 | /// |
175 | /// The following code snippet shows how one can request focus on a |
176 | /// certain widget. |
177 | /// |
178 | /// ```dart |
179 | /// class MyWidget extends StatefulWidget { |
180 | /// const MyWidget({super.key}); |
181 | /// |
182 | /// @override |
183 | /// State<MyWidget> createState() => _MyWidgetState(); |
184 | /// } |
185 | /// |
186 | /// class _MyWidgetState extends State<MyWidget> { |
187 | /// final GlobalKey mykey = GlobalKey(); |
188 | /// |
189 | /// @override |
190 | /// void initState() { |
191 | /// super.initState(); |
192 | /// // Using addPostFrameCallback because changing focus need to wait for the widget to finish rendering. |
193 | /// WidgetsBinding.instance.addPostFrameCallback((_) { |
194 | /// mykey.currentContext?.findRenderObject()?.sendSemanticsEvent(const FocusSemanticEvent()); |
195 | /// }); |
196 | /// } |
197 | /// |
198 | /// @override |
199 | /// Widget build(BuildContext context) { |
200 | /// return Scaffold( |
201 | /// appBar: AppBar( |
202 | /// title: const Text('example'), |
203 | /// ), |
204 | /// body: Column( |
205 | /// children: <Widget>[ |
206 | /// const Text('Hello World'), |
207 | /// const SizedBox(height: 50), |
208 | /// Text('set focus here', key: mykey), |
209 | /// ], |
210 | /// ), |
211 | /// ); |
212 | /// } |
213 | /// } |
214 | /// ``` |
215 | /// {@end-tool} |
216 | /// |
217 | /// This currently only supports Android and iOS. |
218 | class FocusSemanticEvent extends SemanticsEvent { |
219 | /// Constructs an event that triggers a focus change by the platform. |
220 | const FocusSemanticEvent() : super('focus' ); |
221 | |
222 | @override |
223 | Map<String, dynamic> getDataMap() => const <String, dynamic>{}; |
224 | } |
225 | |