Unverified Commit 23a2fa31 authored by chunhtai's avatar chunhtai Committed by GitHub

Reland "Adds API in semanticsconfiguration to decide how to merge chi… (#116895)

* Reland "Adds API in semanticsconfiguration to decide how to merge child semanticsConfigurations (#110730)"

This reverts commit 7549925c.

* makes markNeedsSemanticsUpdate more robust

* address comment
parent ab47fc30
......@@ -1329,6 +1329,35 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
return Size.zero;
}
ChildSemanticsConfigurationsResult _childSemanticsConfigurationDelegate(List<SemanticsConfiguration> childConfigs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
List<SemanticsConfiguration>? prefixMergeGroup;
List<SemanticsConfiguration>? suffixMergeGroup;
for (final SemanticsConfiguration childConfig in childConfigs) {
if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
prefixMergeGroup ??= <SemanticsConfiguration>[];
prefixMergeGroup.add(childConfig);
} else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
suffixMergeGroup ??= <SemanticsConfiguration>[];
suffixMergeGroup.add(childConfig);
} else {
builder.markAsMergeUp(childConfig);
}
}
if (prefixMergeGroup != null) {
builder.markAsSiblingMergeGroup(prefixMergeGroup);
}
if (suffixMergeGroup != null) {
builder.markAsSiblingMergeGroup(suffixMergeGroup);
}
return builder.build();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.childConfigurationsDelegate = _childSemanticsConfigurationDelegate;
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
......@@ -1716,12 +1745,16 @@ class _AffixText extends StatelessWidget {
this.text,
this.style,
this.child,
this.semanticsSortKey,
required this.semanticsTag,
});
final bool labelIsFloating;
final String? text;
final TextStyle? style;
final Widget? child;
final SemanticsSortKey? semanticsSortKey;
final SemanticsTag semanticsTag;
@override
Widget build(BuildContext context) {
......@@ -1731,8 +1764,12 @@ class _AffixText extends StatelessWidget {
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: labelIsFloating ? 1.0 : 0.0,
child: Semantics(
sortKey: semanticsSortKey,
tagForChildren: semanticsTag,
child: child ?? (text == null ? null : Text(text!, style: style)),
),
),
);
}
}
......@@ -1903,6 +1940,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
late final Animation<double> _floatingLabelAnimation;
late final AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = _InputBorderGap();
static const OrdinalSortKey _kPrefixSemanticsSortOrder = OrdinalSortKey(0);
static const OrdinalSortKey _kInputSemanticsSortOrder = OrdinalSortKey(1);
static const OrdinalSortKey _kSuffixSemanticsSortOrder = OrdinalSortKey(2);
static const SemanticsTag _kPrefixSemanticsTag = SemanticsTag('_InputDecoratorState.prefix');
static const SemanticsTag _kSuffixSemanticsTag = SemanticsTag('_InputDecoratorState.suffix');
@override
void initState() {
......@@ -2227,22 +2269,42 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
),
);
final Widget? prefix = decoration.prefix == null && decoration.prefixText == null ? null :
_AffixText(
final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
final bool hasSuffix = decoration.suffix != null || decoration.suffixText != null;
Widget? input = widget.child;
// If at least two out of the three are visible, it needs semantics sort
// order.
final bool needsSemanticsSortOrder = widget._labelShouldWithdraw && (input != null ? (hasPrefix || hasSuffix) : (hasPrefix && hasSuffix));
final Widget? prefix = hasPrefix
? _AffixText(
labelIsFloating: widget._labelShouldWithdraw,
text: decoration.prefixText,
style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
semanticsSortKey: needsSemanticsSortOrder ? _kPrefixSemanticsSortOrder : null,
semanticsTag: _kPrefixSemanticsTag,
child: decoration.prefix,
);
)
: null;
final Widget? suffix = decoration.suffix == null && decoration.suffixText == null ? null :
_AffixText(
final Widget? suffix = hasSuffix
? _AffixText(
labelIsFloating: widget._labelShouldWithdraw,
text: decoration.suffixText,
style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
semanticsSortKey: needsSemanticsSortOrder ? _kSuffixSemanticsSortOrder : null,
semanticsTag: _kSuffixSemanticsTag,
child: decoration.suffix,
);
)
: null;
if (input != null && needsSemanticsSortOrder) {
input = Semantics(
sortKey: _kInputSemanticsSortOrder,
child: input,
);
}
final bool decorationIsDense = decoration.isDense ?? false;
final double iconSize = decorationIsDense ? 18.0 : 24.0;
......@@ -2281,7 +2343,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getPrefixIconColor(themeData, defaults),
size: iconSize,
),
child: decoration.prefixIcon!,
child: Semantics(
child: decoration.prefixIcon,
),
),
),
),
......@@ -2306,7 +2370,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getSuffixIconColor(themeData, defaults),
size: iconSize,
),
child: decoration.suffixIcon!,
child: Semantics(
child: decoration.suffixIcon,
),
),
),
),
......@@ -2383,7 +2449,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
input: widget.child,
input: input,
label: label,
hint: hint,
prefix: prefix,
......
......@@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'dart:ui' as ui;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flutter/services.dart';
......@@ -53,6 +54,20 @@ typedef SemanticsActionHandler = void Function(Object? args);
/// Used by [SemanticsOwner.onSemanticsUpdate].
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
/// Signature for the [SemanticsConfiguration.childConfigurationsDelegate].
///
/// The input list contains all [SemanticsConfiguration]s that rendering
/// children want to merge upward. One can tag a render child with a
/// [SemanticsTag] and look up its [SemanticsConfiguration]s through
/// [SemanticsConfiguration.tagsChildrenWith].
///
/// The return value is the arrangement of these configs, including which
/// configs continue to merge upward and which configs form sibling merge group.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return
/// value.
typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
......@@ -85,6 +100,89 @@ class SemanticsTag {
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
}
/// The result that contains the arrangement for the child
/// [SemanticsConfiguration]s.
///
/// When the [PipelineOwner] builds the semantics tree, it uses the returned
/// [ChildSemanticsConfigurationsResult] from
/// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes
/// should form.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to build the result.
class ChildSemanticsConfigurationsResult {
ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups);
/// Returns the [SemanticsConfiguration]s that are supposed to be merged into
/// the parent semantics node.
///
/// [SemanticsConfiguration]s that are either semantics boundaries or are
/// conflicting with other [SemanticsConfiguration]s will form explicit
/// semantics nodes. All others will be merged into the parent.
final List<SemanticsConfiguration> mergeUp;
/// The groups of child semantics configurations that want to merge together
/// and form a sibling [SemanticsNode].
///
/// All the [SemanticsConfiguration]s in a given group that are either
/// semantics boundaries or are conflicting with other
/// [SemanticsConfiguration]s of the same group will be excluded from the
/// sibling merge group and form independent semantics nodes as usual.
///
/// The result [SemanticsNode]s from the merges are attached as the sibling
/// nodes of the immediate parent semantics node. For example, a `RenderObjectA`
/// has a rendering child, `RenderObjectB`. If both of them form their own
/// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node
/// created from sibling merge groups of `RenderObjectB` will be attach to
/// `SemanticsNodeA` as a sibling of `SemanticsNodeB`.
final List<List<SemanticsConfiguration>> siblingMergeGroups;
}
/// The builder to build a [ChildSemanticsConfigurationsResult] based on its
/// annotations.
///
/// To use this builder, one can use [markAsMergeUp] and
/// [markAsSiblingMergeGroup] to annotate the arrangement of
/// [SemanticsConfiguration]s. Once all the configs are annotated, use [build]
/// to generate the [ChildSemanticsConfigurationsResult].
class ChildSemanticsConfigurationsResultBuilder {
/// Creates a [ChildSemanticsConfigurationsResultBuilder].
ChildSemanticsConfigurationsResultBuilder();
final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[];
final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[];
/// Marks the [SemanticsConfiguration] to be merged into the parent semantics
/// node.
///
/// The [SemanticsConfiguration] will be added to the
/// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds.
void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config);
/// Marks a group of [SemanticsConfiguration]s to merge together
/// and form a sibling [SemanticsNode].
///
/// The group of [SemanticsConfiguration]s will be added to the
/// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds.
void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs);
/// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement.
ChildSemanticsConfigurationsResult build() {
assert((){
final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{};
for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) {
assert(
seenConfigs.add(config),
'Duplicated SemanticsConfigurations. This can happen if the same '
'SemanticsConfiguration was marked twice in markAsMergeUp and/or '
'markAsSiblingMergeGroup'
);
}
return true;
}());
return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups);
}
}
/// An identifier of a custom semantics action.
///
/// Custom semantics actions can be provided to make complex user
......@@ -3724,6 +3822,25 @@ class SemanticsConfiguration {
_onDidLoseAccessibilityFocus = value;
}
/// A delegate that decides how to handle [SemanticsConfiguration]s produced
/// in the widget subtree.
///
/// The [SemanticsConfiguration]s are produced by rendering objects in the
/// subtree and want to merge up to their parent. This delegate can decide
/// which of these should be merged together to form sibling SemanticsNodes and
/// which of them should be merged upwards into the parent SemanticsNode.
///
/// The input list of [SemanticsConfiguration]s can be empty if the rendering
/// object of this semantics configuration is a leaf node.
ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
assert(value != null);
_childConfigurationsDelegate = value;
// Setting the childConfigsDelegate does not annotate any meaningful
// semantics information of the config.
}
/// Returns the action handler registered for [action] or null if none was
/// registered.
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
......@@ -4448,6 +4565,11 @@ class SemanticsConfiguration {
/// * [addTagForChildren] to add a tag and for more information about their
/// usage.
Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren;
/// Whether this configuration will tag the child semantics nodes with a
/// given [SemanticsTag].
bool tagsChildrenWith(SemanticsTag tag) => _tagsForChildren?.contains(tag) ?? false;
Set<SemanticsTag>? _tagsForChildren;
/// Specifies a [SemanticsTag] that this configuration wants to apply to all
......
......@@ -4375,6 +4375,47 @@ void main() {
expect(prefixText.style, prefixStyle);
});
testWidgets('TextField prefix and suffix create a sibling node', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
overlay(
child: TextField(
controller: TextEditingController(text: 'some text'),
decoration: const InputDecoration(
prefixText: 'Prefix',
suffixText: 'Suffix',
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 2,
textDirection: TextDirection.ltr,
label: 'Prefix',
),
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
value: 'some text',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
),
TestSemantics.rootChild(
id: 3,
textDirection: TextDirection.ltr,
label: 'Suffix',
),
],
), ignoreTransform: true, ignoreRect: true));
});
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
final TextStyle suffixStyle = TextStyle(
color: Colors.pink[500],
......
......@@ -1429,6 +1429,51 @@ void main() {
handle.dispose();
});
testWidgets('Two panel semantics is added to the sibling nodes of direct children', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final UniqueKey key = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ListView(
key: key,
children: const <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
prefixText: 'prefix',
),
),
],
),
),
));
// Wait for focus.
await tester.pumpAndSettle();
final SemanticsNode scrollableNode = tester.getSemantics(find.byKey(key));
SemanticsNode? intermediateNode;
scrollableNode.visitChildren((SemanticsNode node) {
intermediateNode = node;
return true;
});
SemanticsNode? syntheticScrollableNode;
intermediateNode!.visitChildren((SemanticsNode node) {
syntheticScrollableNode = node;
return true;
});
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
int numberOfChild = 0;
syntheticScrollableNode!.visitChildren((SemanticsNode node) {
expect(node.isTagged(RenderViewport.useTwoPaneSemantics), isTrue);
numberOfChild += 1;
return true;
});
expect(numberOfChild, 2);
handle.dispose();
});
testWidgets('Scroll inertia cancel event', (WidgetTester tester) async {
await pumpTest(tester, null);
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment