semantics_event.dart 7.35 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6
import 'package:flutter/foundation.dart';
7 8
import 'package:flutter/painting.dart';

9 10
export 'dart:ui' show TextDirection;

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
/// Determines the assertiveness level of the accessibility announcement.
///
/// It is used by [AnnounceSemanticsEvent] to determine the priority with which
/// assistive technology should treat announcements.
enum Assertiveness {
  /// The assistive technology will speak changes whenever the user is idle.
  polite,

  /// The assistive technology will interrupt any announcement that it is
  /// currently making to notify the user about the change.
  ///
  /// It should only be used for time-sensitive/critical notifications.
  assertive,
}

26 27
/// An event sent by the application to notify interested listeners that
/// something happened to the user interface (e.g. a view scrolled).
28 29 30 31 32 33 34
///
/// These events are usually interpreted by assistive technologies to give the
/// user additional clues about the current state of the UI.
abstract class SemanticsEvent {
  /// Initializes internal fields.
  ///
  /// [type] is a string that identifies this class of [SemanticsEvent]s.
35
  const SemanticsEvent(this.type);
36 37 38 39 40 41 42 43 44 45

  /// The type of this event.
  ///
  /// The type is used by the engine to translate this event into the
  /// appropriate native event (`UIAccessibility*Notification` on iOS and
  /// `AccessibilityEvent` on Android).
  final String type;

  /// Converts this event to a Map that can be encoded with
  /// [StandardMessageCodec].
46 47 48
  ///
  /// [nodeId] is the unique identifier of the semantics node associated with
  /// the event, or null if the event is not associated with a semantics node.
49
  Map<String, dynamic> toMap({ int? nodeId }) {
50 51 52 53
    final Map<String, dynamic> event = <String, dynamic>{
      'type': type,
      'data': getDataMap(),
    };
54
    if (nodeId != null) {
55
      event['nodeId'] = nodeId;
56
    }
57 58 59 60 61 62

    return event;
  }

  /// Returns the event's data object.
  Map<String, dynamic> getDataMap();
63 64 65 66

  @override
  String toString() {
    final List<String> pairs = <String>[];
67 68
    final Map<String, dynamic> dataMap = getDataMap();
    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 77 78 79 80 81 82 83 84 85 86 87 88
/// An event for a semantic announcement.
///
/// This should be used for announcement that are not seamlessly announced by
/// the system as a result of a UI state change.
///
/// For example a camera application can use this method to make accessibility
/// announcements regarding objects in the viewfinder.
///
/// When possible, prefer using mechanisms like [Semantics] to implicitly
/// trigger announcements over using this event.
class AnnounceSemanticsEvent extends SemanticsEvent {

  /// 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 93 94 95 96 97 98 99 100 101

  /// The message to announce.
  ///
  /// This property must not be null.
  final String message;

  /// Text direction for [message].
  ///
  /// This property must not be null.
  final TextDirection textDirection;

102 103 104 105 106 107 108 109
  /// Determines whether the announcement should interrupt any existing announcement,
  /// or queue after it.
  ///
  /// On the web this option uses the aria-live level to set the assertiveness
  /// of the announcement. On iOS, Android, Windows, Linux, macOS, and Fuchsia
  /// this option currently has no effect.
  final Assertiveness assertiveness;

110 111
  @override
  Map<String, dynamic> getDataMap() {
112
    return <String, dynamic> {
113 114
      'message': message,
      'textDirection': textDirection.index,
115 116
      if (assertiveness != Assertiveness.polite)
        'assertiveness': assertiveness.index,
117 118 119
    };
  }
}
120 121

/// An event for a semantic announcement of a tooltip.
122
///
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
/// This is only used by Android to announce tooltip values.
class TooltipSemanticsEvent extends SemanticsEvent {
  /// Constructs an event that triggers a tooltip announcement by the platform.
  const TooltipSemanticsEvent(this.message) : super('tooltip');

  /// The text content of the tooltip.
  final String message;

  @override
  Map<String, dynamic> getDataMap() {
    return <String, dynamic>{
      'message': message,
    };
  }
}
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161

/// An event which triggers long press semantic feedback.
///
/// Currently only honored on Android. Triggers a long-press specific sound
/// when TalkBack is enabled.
class LongPressSemanticsEvent extends SemanticsEvent {
  /// Constructs an event that triggers a long-press semantic feedback by the platform.
  const LongPressSemanticsEvent() : super('longPress');

  @override
  Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}

/// An event which triggers tap semantic feedback.
///
/// Currently only honored on Android. Triggers a tap specific sound when
/// TalkBack is enabled.
class TapSemanticEvent extends SemanticsEvent {
  /// Constructs an event that triggers a long-press semantic feedback by the platform.
  const TapSemanticEvent() : super('tap');

  @override
  Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

/// An event to move the accessibility focus.
///
/// Using this API is generally not recommended, as it may break a users' expectation of
/// how a11y focus works and therefore should be just very carefully.
///
/// One possibile use case:
/// For example, the currently focused rendering object is replaced by another rendering
/// object. In general, such design should be avoided if possible. If not, one may want
/// to refocus the newly added rendering object.
///
/// One example that is not recommended:
/// When a new popup or dropdown opens, moving the focus in these cases may confuse users
/// and make it less accessible.
///
/// {@tool snippet}
///
/// The following code snippet shows how one can request focus on a
/// certain widget.
///
/// ```dart
/// class MyWidget extends StatefulWidget {
///   const MyWidget({super.key});
///
///   @override
///   State<MyWidget> createState() => _MyWidgetState();
/// }
///
/// class _MyWidgetState extends State<MyWidget> {
///   final GlobalKey mykey = GlobalKey();
///
///   @override
///   void initState() {
///     super.initState();
///     // Using addPostFrameCallback because changing focus need to wait for the widget to finish rendering.
///     WidgetsBinding.instance.addPostFrameCallback((_) {
///       mykey.currentContext?.findRenderObject()?.sendSemanticsEvent(const FocusSemanticEvent());
///     });
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return Scaffold(
///       appBar: AppBar(
///         title: const Text('example'),
///       ),
///       body: Column(
///         children: <Widget>[
///           const Text('Hello World'),
///           const SizedBox(height: 50),
///           Text('set focus here', key: mykey),
///         ],
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
///
/// This currently only supports Android and iOS.
class FocusSemanticEvent extends SemanticsEvent {
  /// Constructs an event that triggers a focus change by the platform.
  const FocusSemanticEvent() : super('focus');

  @override
  Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}