diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index fdee268f2916dc..ee183a51d3b791 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -4356,6 +4356,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { if (_properties.image != null) { config.isImage = _properties.image!; } + if (_properties.identifier != null) { + config.identifier = _properties.identifier!; + } if (_attributedLabel != null) { config.attributedLabel = _attributedLabel!; } diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index 9f1353900b897f..5f264f12b788b2 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -2,14 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder; +// ignore: deprecated_member_use +import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'debug.dart'; -export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder; +// ignore: deprecated_member_use +export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew; /// The glue between the semantics layer and the Flutter engine. mixin SemanticsBinding on BindingBase { @@ -160,8 +162,10 @@ mixin SemanticsBinding on BindingBase { /// /// This method is used by the [SemanticsOwner] to create builder for all its /// semantics updates. - ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() { - return ui.SemanticsUpdateBuilder(); + // ignore: deprecated_member_use + ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() { + // ignore: deprecated_member_use + return ui.SemanticsUpdateBuilderNew(); } /// The platform is requesting that animations be disabled or simplified. diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index a3a29f1ca16ea7..7a0b29b4e126ff 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -3,7 +3,8 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilder, StringAttribute, TextDirection; +// ignore: deprecated_member_use +import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilderNew, StringAttribute, TextDirection; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -426,6 +427,7 @@ class SemanticsData with Diagnosticable { SemanticsData({ required this.flags, required this.actions, + required this.identifier, required this.attributedLabel, required this.attributedValue, required this.attributedIncreasedValue, @@ -461,6 +463,9 @@ class SemanticsData with Diagnosticable { /// A bit field of [SemanticsAction]s that apply to this node. final int actions; + /// {@macro flutter.semantics.SemanticsProperties.identifier} + final String identifier; + /// A textual description for the current label of the node. /// /// The reading direction is given by [textDirection]. @@ -696,6 +701,7 @@ class SemanticsData with Diagnosticable { flag.name, ]; properties.add(IterableProperty('flags', flagSummary, ifEmpty: null)); + properties.add(StringProperty('identifier', identifier, defaultValue: '')); properties.add(AttributedStringProperty('label', attributedLabel)); properties.add(AttributedStringProperty('value', attributedValue)); properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue)); @@ -721,6 +727,7 @@ class SemanticsData with Diagnosticable { return other is SemanticsData && other.flags == flags && other.actions == actions + && other.identifier == identifier && other.attributedLabel == attributedLabel && other.attributedValue == attributedValue && other.attributedIncreasedValue == attributedIncreasedValue @@ -749,6 +756,7 @@ class SemanticsData with Diagnosticable { int get hashCode => Object.hash( flags, actions, + identifier, attributedLabel, attributedValue, attributedIncreasedValue, @@ -765,8 +773,8 @@ class SemanticsData with Diagnosticable { scrollExtentMax, scrollExtentMin, platformViewId, - maxValueLength, Object.hash( + maxValueLength, currentValueLength, transform, elevation, @@ -901,6 +909,7 @@ class SemanticsProperties extends DiagnosticableTree { this.liveRegion, this.maxValueLength, this.currentValueLength, + this.identifier, this.label, this.attributedLabel, this.value, @@ -1165,6 +1174,21 @@ class SemanticsProperties extends DiagnosticableTree { /// [maxValueLength] is set. final int? currentValueLength; + /// {@template flutter.semantics.SemanticsProperties.identifier} + /// Provides an identifier for the semantics node in native accessibility hierarchy. + /// + /// This value is not exposed to the users of the app. + /// + /// It's usually used for UI testing with tools that work by querying the + /// native accessibility, like UIAutomator, XCUITest, or Appium. + /// + /// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`. + /// It'll be appear in accessibility hierarchy as `resource-id`. + /// + /// On iOS, this will set `UIAccessibilityElement.accessibilityIdentifier`. + /// {@endtemplate} + final String? identifier; + /// Provides a textual description of the widget. /// /// If a label is provided, there must either by an ambient [Directionality] @@ -1632,6 +1656,7 @@ class SemanticsProperties extends DiagnosticableTree { properties.add(DiagnosticsProperty('mixed', mixed, defaultValue: null)); properties.add(DiagnosticsProperty('expanded', expanded, defaultValue: null)); properties.add(DiagnosticsProperty('selected', selected, defaultValue: null)); + properties.add(StringProperty('identifier', identifier)); properties.add(StringProperty('label', label, defaultValue: null)); properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null)); properties.add(StringProperty('value', value, defaultValue: null)); @@ -2210,6 +2235,10 @@ class SemanticsNode with DiagnosticableTreeMixin { /// Whether this node currently has a given [SemanticsFlag]. bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0; + /// {@macro flutter.semantics.SemanticsProperties.identifier} + String get identifier => _identifier; + String _identifier = _kEmptyConfig.identifier; + /// A textual description of this node. /// /// The reading direction is given by [textDirection]. @@ -2514,6 +2543,7 @@ class SemanticsNode with DiagnosticableTreeMixin { final bool mergeAllDescendantsIntoThisNodeValueChanged = _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants; + _identifier = config.identifier; _attributedLabel = config.attributedLabel; _attributedValue = config.attributedValue; _attributedIncreasedValue = config.attributedIncreasedValue; @@ -2569,6 +2599,7 @@ class SemanticsNode with DiagnosticableTreeMixin { // Can't use _effectiveActionsAsBits here. The filtering of action bits // must be done after the merging the its descendants. int actions = _actionsAsBits; + String identifier = _identifier; AttributedString attributedLabel = _attributedLabel; AttributedString attributedValue = _attributedValue; AttributedString attributedIncreasedValue = _attributedIncreasedValue; @@ -2625,6 +2656,9 @@ class SemanticsNode with DiagnosticableTreeMixin { platformViewId ??= node._platformViewId; maxValueLength ??= node._maxValueLength; currentValueLength ??= node._currentValueLength; + if (identifier == '') { + identifier = node._identifier; + } if (attributedValue.string == '') { attributedValue = node._attributedValue; } @@ -2682,6 +2716,7 @@ class SemanticsNode with DiagnosticableTreeMixin { return SemanticsData( flags: flags, actions: _areUserActionsBlocked ? actions & _kUnblockedUserActions : actions, + identifier: identifier, attributedLabel: attributedLabel, attributedValue: attributedValue, attributedIncreasedValue: attributedIncreasedValue, @@ -2715,7 +2750,8 @@ class SemanticsNode with DiagnosticableTreeMixin { static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0); static final Float64List _kIdentityTransform = _initIdentityTransform(); - void _addToUpdate(SemanticsUpdateBuilder builder, Set customSemanticsActionIdsUpdate) { + // ignore: deprecated_member_use + void _addToUpdate(SemanticsUpdateBuilderNew builder, Set customSemanticsActionIdsUpdate) { assert(_dirty); final SemanticsData data = getSemanticsData(); final Int32List childrenInTraversalOrder; @@ -2750,6 +2786,7 @@ class SemanticsNode with DiagnosticableTreeMixin { flags: data.flags, actions: data.actions, rect: data.rect, + identifier: data.identifier, label: data.attributedLabel.string, labelAttributes: data.attributedLabel.attributes, value: data.attributedValue.string, @@ -2904,6 +2941,7 @@ class SemanticsNode with DiagnosticableTreeMixin { properties.add(IterableProperty('flags', flags, ifEmpty: null)); properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible')); properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN')); + properties.add(StringProperty('identifier', _identifier, defaultValue: '')); properties.add(AttributedStringProperty('label', _attributedLabel)); properties.add(AttributedStringProperty('value', _attributedValue)); properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue)); @@ -3406,7 +3444,8 @@ class SemanticsOwner extends ChangeNotifier { } } visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); - final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder(); + // ignore: deprecated_member_use + final SemanticsUpdateBuilderNew builder = SemanticsBinding.instance.createSemanticsUpdateBuilder(); for (final SemanticsNode node in visitedNodes) { assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) // The _serialize() method marks the node as not dirty, and @@ -4201,6 +4240,14 @@ class SemanticsConfiguration { } } + /// {@macro flutter.semantics.SemanticsProperties.identifier} + String get identifier => _identifier; + String _identifier = ''; + set identifier(String identifier) { + _identifier = identifier; + _hasBeenAnnotated = true; + } + /// A textual description of the owning [RenderObject]. /// /// Setting this attribute will override the [attributedLabel]. @@ -4898,6 +4945,9 @@ class SemanticsConfiguration { textDirection ??= child.textDirection; _sortKey ??= child._sortKey; + if (_identifier == '') { + _identifier = child._identifier; + } _attributedLabel = _concatAttributedString( thisAttributedString: _attributedLabel, thisTextDirection: textDirection, @@ -4938,6 +4988,7 @@ class SemanticsConfiguration { .._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants .._textDirection = _textDirection .._sortKey = _sortKey + .._identifier = _identifier .._attributedLabel = _attributedLabel .._attributedIncreasedValue = _attributedIncreasedValue .._attributedValue = _attributedValue diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index b4aa4d84df9e03..f07c8af9040f94 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -7123,6 +7123,7 @@ class Semantics extends SingleChildRenderObjectWidget { bool? expanded, int? maxValueLength, int? currentValueLength, + String? identifier, String? label, AttributedString? attributedLabel, String? value, @@ -7191,6 +7192,7 @@ class Semantics extends SingleChildRenderObjectWidget { liveRegion: liveRegion, maxValueLength: maxValueLength, currentValueLength: currentValueLength, + identifier: identifier, label: label, attributedLabel: attributedLabel, value: value, diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index a452fea0a637df..ec6ae2d1fd2eb1 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -682,6 +682,7 @@ void main() { ' flags: []\n' ' invisible\n' ' isHidden: false\n' + ' identifier: ""\n' ' label: ""\n' ' value: ""\n' ' increasedValue: ""\n' @@ -805,6 +806,7 @@ void main() { ' flags: []\n' ' invisible\n' ' isHidden: false\n' + ' identifier: ""\n' ' label: ""\n' ' value: ""\n' ' increasedValue: ""\n' diff --git a/packages/flutter/test/semantics/semantics_update_test.dart b/packages/flutter/test/semantics/semantics_update_test.dart index 42708c877974cd..57bf081e03585b 100644 --- a/packages/flutter/test/semantics/semantics_update_test.dart +++ b/packages/flutter/test/semantics/semantics_update_test.dart @@ -157,6 +157,7 @@ void main() { 'Semantics(' 'container: false, ' 'properties: SemanticsProperties, ' + 'identifier: null, '// ignore: missing_whitespace_between_adjacent_strings 'attributedLabel: "label" [SpellOutStringAttribute(TextRange(start: 0, end: 5))], ' 'attributedValue: "value" [LocaleStringAttribute(TextRange(start: 0, end: 5), en-MX)], ' 'attributedHint: "hint" [SpellOutStringAttribute(TextRange(start: 1, end: 2))], ' @@ -171,13 +172,16 @@ void main() { class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding { @override - ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() { + // ignore: deprecated_member_use + ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() { return SemanticsUpdateBuilderSpy(); } } -class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilder { - final SemanticsUpdateBuilder _builder = ui.SemanticsUpdateBuilder(); +// ignore: deprecated_member_use +class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilderNew { + // ignore: deprecated_member_use + final SemanticsUpdateBuilderNew _builder = ui.SemanticsUpdateBuilderNew(); static Map observations = {}; @@ -199,6 +203,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde required double elevation, required double thickness, required Rect rect, + required String identifier, required String label, List? labelAttributes, required String value, diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 0e737f17b63de8..59308ccd289373 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -624,6 +624,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) { /// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. /// * [containsSemantics], a similar matcher without default values for flags or actions. Matcher matchesSemantics({ + String? identifier, String? label, AttributedString? attributedLabel, String? hint, @@ -701,6 +702,7 @@ Matcher matchesSemantics({ List? children, }) { return _MatchesSemanticsData( + identifier: identifier, label: label, attributedLabel: attributedLabel, hint: hint, @@ -808,6 +810,7 @@ Matcher matchesSemantics({ /// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. /// * [matchesSemantics], a similar matcher with default values for flags and actions. Matcher containsSemantics({ + String? identifier, String? label, AttributedString? attributedLabel, String? hint, @@ -885,6 +888,7 @@ Matcher containsSemantics({ List? children, }) { return _MatchesSemanticsData( + identifier: identifier, label: label, attributedLabel: attributedLabel, hint: hint, @@ -2207,6 +2211,7 @@ class _MatchesReferenceImage extends AsyncMatcher { class _MatchesSemanticsData extends Matcher { _MatchesSemanticsData({ + required this.identifier, required this.label, required this.attributedLabel, required this.hint, @@ -2344,6 +2349,7 @@ class _MatchesSemanticsData extends Matcher { onLongPressHint: onLongPressHint, ); + final String? identifier; final String? label; final AttributedString? attributedLabel; final String? hint; diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 36598dc796c21a..dd3f08f707da97 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -663,6 +663,7 @@ void main() { final SemanticsData data = SemanticsData( flags: flags, actions: actions, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), @@ -790,6 +791,7 @@ void main() { link: true, onTap: () { }, onLongPress: () { }, + identifier: 'ident', label: 'foo', hint: 'bar', value: 'baz', @@ -947,6 +949,7 @@ void main() { final SemanticsData data = SemanticsData( flags: flags, actions: actions, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), @@ -1039,6 +1042,7 @@ void main() { final SemanticsData data = SemanticsData( flags: 0, actions: 0, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), @@ -1137,6 +1141,7 @@ void main() { final SemanticsData emptyData = SemanticsData( flags: 0, actions: 0, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), @@ -1163,6 +1168,7 @@ void main() { final SemanticsData fullData = SemanticsData( flags: allFlags, actions: allActions, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), @@ -1252,6 +1258,7 @@ void main() { final SemanticsData data = SemanticsData( flags: 0, actions: SemanticsAction.customAction.index, + identifier: 'i', attributedLabel: AttributedString('a'), attributedIncreasedValue: AttributedString('b'), attributedValue: AttributedString('c'), diff --git a/packages/flutter_test/test/view_test.dart b/packages/flutter_test/test/view_test.dart index 2b245a39303eff..4d29996acdece8 100644 --- a/packages/flutter_test/test/view_test.dart +++ b/packages/flutter_test/test/view_test.dart @@ -313,7 +313,8 @@ void main() { }); testWidgets('updateSemantics is passed through to backing FlutterView', (WidgetTester tester) async { - final SemanticsUpdate expectedUpdate = SemanticsUpdateBuilder().build(); + // ignore: deprecated_member_use + final SemanticsUpdate expectedUpdate = SemanticsUpdateBuilderNew().build(); final _FakeFlutterView backingView = _FakeFlutterView(); final TestFlutterView view = TestFlutterView( view: backingView,