Adds API in semanticsconfiguration to decide how to merge child semanticsConfigurations (#110730)
* Adds semantics merger API and fix input decorator * addressing comments * abstractnode to object * feature complete * addressing comments * fix comments * conditionally add sort order * fix bool * fix test * more fix * fix tests
This commit is contained in:
parent
182f9f666f
commit
352ad3a9ef
@ -1326,6 +1326,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;
|
||||
@ -1713,12 +1742,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) {
|
||||
@ -1728,8 +1761,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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1899,6 +1936,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
late AnimationController _floatingLabelController;
|
||||
late 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() {
|
||||
@ -2218,22 +2260,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;
|
||||
@ -2272,7 +2334,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
color: _getPrefixIconColor(themeData, defaults),
|
||||
size: iconSize,
|
||||
),
|
||||
child: decoration.prefixIcon!,
|
||||
child: Semantics(
|
||||
child: decoration.prefixIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -2297,7 +2361,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
||||
color: _getSuffixIconColor(themeData, defaults),
|
||||
size: iconSize,
|
||||
),
|
||||
child: decoration.suffixIcon!,
|
||||
child: Semantics(
|
||||
child: decoration.suffixIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -2374,7 +2440,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,
|
||||
|
@ -3100,6 +3100,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
if (_cachedSemanticsConfiguration == null) {
|
||||
_cachedSemanticsConfiguration = SemanticsConfiguration();
|
||||
describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
|
||||
assert(
|
||||
!_cachedSemanticsConfiguration!.explicitChildNodes || _cachedSemanticsConfiguration!.childConfigurationsDelegate == null,
|
||||
'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
|
||||
);
|
||||
}
|
||||
return _cachedSemanticsConfiguration!;
|
||||
}
|
||||
@ -3161,7 +3165,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
|
||||
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
|
||||
_cachedSemanticsConfiguration = null;
|
||||
bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary;
|
||||
// The childConfigurationsDelegate may produce sibling nodes to be attached
|
||||
// to the parent of this semantics node, thus it can't be a semantics
|
||||
// boundary.
|
||||
bool isEffectiveSemanticsBoundary =
|
||||
_semanticsConfiguration.childConfigurationsDelegate == null &&
|
||||
_semanticsConfiguration.isSemanticBoundary &&
|
||||
wasSemanticsBoundary;
|
||||
RenderObject node = this;
|
||||
|
||||
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
|
||||
@ -3213,11 +3223,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
assert(fragment is _InterestingSemanticsFragment);
|
||||
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
|
||||
final List<SemanticsNode> result = <SemanticsNode>[];
|
||||
final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
|
||||
interestingFragment.compileChildren(
|
||||
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
|
||||
parentPaintClipRect: _semantics?.parentPaintClipRect,
|
||||
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
|
||||
result: result,
|
||||
siblingNodes: siblingNodes,
|
||||
);
|
||||
final SemanticsNode node = result.single;
|
||||
// Fragment only wants to add this node's SemanticsNode to the parent.
|
||||
@ -3235,70 +3247,94 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
|
||||
|
||||
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
|
||||
final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
|
||||
final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = <_InterestingSemanticsFragment>{};
|
||||
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
|
||||
|
||||
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
|
||||
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
|
||||
final bool hasChildConfigurationsDelegate = config.childConfigurationsDelegate != null;
|
||||
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
|
||||
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
|
||||
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
|
||||
visitChildrenForSemantics((RenderObject renderChild) {
|
||||
assert(!_needsLayout);
|
||||
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
|
||||
mergeIntoParent: childrenMergeIntoParent,
|
||||
);
|
||||
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
|
||||
fragments.clear();
|
||||
toBeMarkedExplicit.clear();
|
||||
childConfigurations.clear();
|
||||
mergeUpFragments.clear();
|
||||
siblingMergeFragmentGroups.clear();
|
||||
if (!config.isSemanticBoundary) {
|
||||
dropSemanticsOfPreviousSiblings = true;
|
||||
}
|
||||
}
|
||||
// Figure out which child fragments are to be made explicit.
|
||||
for (final _InterestingSemanticsFragment fragment in parentFragment.interestingFragments) {
|
||||
fragments.add(fragment);
|
||||
for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
|
||||
fragment.addAncestor(this);
|
||||
fragment.addTags(config.tagsForChildren);
|
||||
if (config.explicitChildNodes || parent is! RenderObject) {
|
||||
fragment.markAsExplicit();
|
||||
continue;
|
||||
if (hasChildConfigurationsDelegate && fragment.config != null) {
|
||||
// This fragment need to go through delegate to determine whether it
|
||||
// merge up or not.
|
||||
childConfigurations.add(fragment.config!);
|
||||
configToFragment[fragment.config!] = fragment;
|
||||
} else {
|
||||
mergeUpFragments.add(fragment);
|
||||
}
|
||||
if (!fragment.hasConfigForParent || producesForkingFragment) {
|
||||
continue;
|
||||
}
|
||||
if (!config.isCompatibleWith(fragment.config)) {
|
||||
toBeMarkedExplicit.add(fragment);
|
||||
}
|
||||
final int siblingLength = fragments.length - 1;
|
||||
for (int i = 0; i < siblingLength; i += 1) {
|
||||
final _InterestingSemanticsFragment siblingFragment = fragments[i];
|
||||
if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
|
||||
toBeMarkedExplicit.add(fragment);
|
||||
toBeMarkedExplicit.add(siblingFragment);
|
||||
if (parentFragment is _ContainerSemanticsFragment) {
|
||||
// Container fragments needs to propagate sibling merge group to be
|
||||
// compiled by _SwitchableSemanticsFragment.
|
||||
for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
|
||||
for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
|
||||
siblingMergingFragment.addAncestor(this);
|
||||
siblingMergingFragment.addTags(config.tagsForChildren);
|
||||
}
|
||||
siblingMergeFragmentGroups.add(siblingMergeGroup);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (final _InterestingSemanticsFragment fragment in toBeMarkedExplicit) {
|
||||
assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
|
||||
|
||||
if (explicitChildNode) {
|
||||
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
|
||||
fragment.markAsExplicit();
|
||||
}
|
||||
} else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
|
||||
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
|
||||
mergeUpFragments.addAll(
|
||||
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
|
||||
);
|
||||
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
|
||||
siblingMergeFragmentGroups.add(
|
||||
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_needsSemanticsUpdate = false;
|
||||
|
||||
_SemanticsFragment result;
|
||||
final _SemanticsFragment result;
|
||||
if (parent is! RenderObject) {
|
||||
assert(!config.hasBeenAnnotated);
|
||||
assert(!mergeIntoParent);
|
||||
assert(siblingMergeFragmentGroups.isEmpty);
|
||||
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
|
||||
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
|
||||
result = _RootSemanticsFragment(
|
||||
owner: this,
|
||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||
);
|
||||
} else if (producesForkingFragment) {
|
||||
result = _ContainerSemanticsFragment(
|
||||
siblingMergeGroups: siblingMergeFragmentGroups,
|
||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||
);
|
||||
} else {
|
||||
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
|
||||
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
|
||||
result = _SwitchableSemanticsFragment(
|
||||
config: config,
|
||||
mergeIntoParent: mergeIntoParent,
|
||||
siblingMergeGroups: siblingMergeFragmentGroups,
|
||||
owner: this,
|
||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||
);
|
||||
@ -3307,12 +3343,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
fragment.markAsExplicit();
|
||||
}
|
||||
}
|
||||
|
||||
result.addAll(fragments);
|
||||
|
||||
result.addAll(mergeUpFragments);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _marksExplicitInMergeGroup(List<_InterestingSemanticsFragment> mergeGroup, {bool isMergeUp = false}) {
|
||||
final Set<_InterestingSemanticsFragment> toBeExplicit = <_InterestingSemanticsFragment>{};
|
||||
for (int i = 0; i < mergeGroup.length; i += 1) {
|
||||
final _InterestingSemanticsFragment fragment = mergeGroup[i];
|
||||
if (!fragment.hasConfigForParent) {
|
||||
continue;
|
||||
}
|
||||
if (isMergeUp && !_semanticsConfiguration.isCompatibleWith(fragment.config)) {
|
||||
toBeExplicit.add(fragment);
|
||||
}
|
||||
final int siblingLength = i;
|
||||
for (int j = 0; j < siblingLength; j += 1) {
|
||||
final _InterestingSemanticsFragment siblingFragment = mergeGroup[j];
|
||||
if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
|
||||
toBeExplicit.add(fragment);
|
||||
toBeExplicit.add(siblingFragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final _InterestingSemanticsFragment fragment in toBeExplicit) {
|
||||
fragment.markAsExplicit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when collecting the semantics of this node.
|
||||
///
|
||||
/// The implementation has to return the children in paint order skipping all
|
||||
@ -3985,8 +4043,9 @@ mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
|
||||
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic
|
||||
/// information of multiple [_InterestingSemanticsFragment] to a parent.
|
||||
abstract class _SemanticsFragment {
|
||||
_SemanticsFragment({ required this.dropsSemanticsOfPreviousSiblings })
|
||||
: assert (dropsSemanticsOfPreviousSiblings != null);
|
||||
_SemanticsFragment({
|
||||
required this.dropsSemanticsOfPreviousSiblings,
|
||||
}) : assert (dropsSemanticsOfPreviousSiblings != null);
|
||||
|
||||
/// Incorporate the fragments of children into this fragment.
|
||||
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
|
||||
@ -4002,25 +4061,29 @@ abstract class _SemanticsFragment {
|
||||
|
||||
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
|
||||
/// information that this fragment wants to add to the parent.
|
||||
List<_InterestingSemanticsFragment> get interestingFragments;
|
||||
List<_InterestingSemanticsFragment> get mergeUpFragments;
|
||||
}
|
||||
|
||||
/// A container used when a [RenderObject] wants to add multiple independent
|
||||
/// [_InterestingSemanticsFragment] to its parent.
|
||||
///
|
||||
/// The [_InterestingSemanticsFragment] to be added to the parent can be
|
||||
/// obtained via [interestingFragments].
|
||||
/// obtained via [mergeUpFragments].
|
||||
class _ContainerSemanticsFragment extends _SemanticsFragment {
|
||||
_ContainerSemanticsFragment({
|
||||
required super.dropsSemanticsOfPreviousSiblings,
|
||||
required this.siblingMergeGroups,
|
||||
});
|
||||
|
||||
_ContainerSemanticsFragment({ required super.dropsSemanticsOfPreviousSiblings });
|
||||
final List<List<_InterestingSemanticsFragment>> siblingMergeGroups;
|
||||
|
||||
@override
|
||||
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
|
||||
interestingFragments.addAll(fragments);
|
||||
mergeUpFragments.addAll(fragments);
|
||||
}
|
||||
|
||||
@override
|
||||
final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[];
|
||||
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
|
||||
}
|
||||
|
||||
/// A [_SemanticsFragment] that describes which concrete semantic information
|
||||
@ -4057,6 +4120,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
||||
required Rect? parentPaintClipRect,
|
||||
required double elevationAdjustment,
|
||||
required List<SemanticsNode> result,
|
||||
required List<SemanticsNode> siblingNodes,
|
||||
});
|
||||
|
||||
/// The [SemanticsConfiguration] the child wants to merge into the parent's
|
||||
@ -4086,7 +4150,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
||||
bool get hasConfigForParent => config != null;
|
||||
|
||||
@override
|
||||
List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this];
|
||||
List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
|
||||
|
||||
Set<SemanticsTag>? _tagsForChildren;
|
||||
|
||||
@ -4124,7 +4188,13 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
});
|
||||
|
||||
@override
|
||||
void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) {
|
||||
void compileChildren({
|
||||
Rect? parentSemanticsClipRect,
|
||||
Rect? parentPaintClipRect,
|
||||
required double elevationAdjustment,
|
||||
required List<SemanticsNode> result,
|
||||
required List<SemanticsNode> siblingNodes,
|
||||
}) {
|
||||
assert(_tagsForChildren == null || _tagsForChildren!.isEmpty);
|
||||
assert(parentSemanticsClipRect == null);
|
||||
assert(parentPaintClipRect == null);
|
||||
@ -4150,8 +4220,11 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
parentPaintClipRect: parentPaintClipRect,
|
||||
elevationAdjustment: 0.0,
|
||||
result: children,
|
||||
siblingNodes: siblingNodes,
|
||||
);
|
||||
}
|
||||
// Root node does not have a parent and thus can't attach sibling nodes.
|
||||
assert(siblingNodes.isEmpty);
|
||||
node.updateWith(config: null, childrenInInversePaintOrder: children);
|
||||
|
||||
// The root node is the only semantics node allowed to be invisible. This
|
||||
@ -4201,9 +4274,11 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_SwitchableSemanticsFragment({
|
||||
required bool mergeIntoParent,
|
||||
required SemanticsConfiguration config,
|
||||
required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
|
||||
required super.owner,
|
||||
required super.dropsSemanticsOfPreviousSiblings,
|
||||
}) : _mergeIntoParent = mergeIntoParent,
|
||||
}) : _siblingMergeGroups = siblingMergeGroups,
|
||||
_mergeIntoParent = mergeIntoParent,
|
||||
_config = config,
|
||||
assert(mergeIntoParent != null),
|
||||
assert(config != null);
|
||||
@ -4211,14 +4286,126 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
final bool _mergeIntoParent;
|
||||
SemanticsConfiguration _config;
|
||||
bool _isConfigWritable = false;
|
||||
bool _mergesToSibling = false;
|
||||
|
||||
final List<List<_InterestingSemanticsFragment>> _siblingMergeGroups;
|
||||
|
||||
void _mergeSiblingGroup(Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, List<SemanticsNode> result, Set<int> usedSemanticsIds) {
|
||||
for (final List<_InterestingSemanticsFragment> group in _siblingMergeGroups) {
|
||||
Rect? rect;
|
||||
Rect? semanticsClipRect;
|
||||
Rect? paintClipRect;
|
||||
SemanticsConfiguration? configuration;
|
||||
// Use empty set because the _tagsForChildren may not contains all of the
|
||||
// tags if this fragment is not explicit. The _tagsForChildren are added
|
||||
// to sibling nodes at the end of compileChildren if this fragment is
|
||||
// explicit.
|
||||
final Set<SemanticsTag> tags = <SemanticsTag>{};
|
||||
SemanticsNode? node;
|
||||
for (final _InterestingSemanticsFragment fragment in group) {
|
||||
if (fragment.config != null) {
|
||||
final _SwitchableSemanticsFragment switchableFragment = fragment as _SwitchableSemanticsFragment;
|
||||
switchableFragment._mergesToSibling = true;
|
||||
node ??= fragment.owner._semantics;
|
||||
if (configuration == null) {
|
||||
switchableFragment._ensureConfigIsWritable();
|
||||
configuration = switchableFragment.config;
|
||||
} else {
|
||||
configuration.absorb(switchableFragment.config!);
|
||||
}
|
||||
// It is a child fragment of a _SwitchableFragment, it must have a
|
||||
// geometry.
|
||||
final _SemanticsGeometry geometry = switchableFragment._computeSemanticsGeometry(
|
||||
parentSemanticsClipRect: parentSemanticsClipRect,
|
||||
parentPaintClipRect: parentPaintClipRect,
|
||||
)!;
|
||||
final Rect fragmentRect = MatrixUtils.transformRect(geometry.transform, geometry.rect);
|
||||
if (rect == null) {
|
||||
rect = fragmentRect;
|
||||
} else {
|
||||
rect = rect.expandToInclude(fragmentRect);
|
||||
}
|
||||
if (geometry.semanticsClipRect != null) {
|
||||
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.semanticsClipRect!);
|
||||
if (semanticsClipRect == null) {
|
||||
semanticsClipRect = rect;
|
||||
} else {
|
||||
semanticsClipRect = semanticsClipRect.intersect(rect);
|
||||
}
|
||||
}
|
||||
if (geometry.paintClipRect != null) {
|
||||
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.paintClipRect!);
|
||||
if (paintClipRect == null) {
|
||||
paintClipRect = rect;
|
||||
} else {
|
||||
paintClipRect = paintClipRect.intersect(rect);
|
||||
}
|
||||
}
|
||||
if (switchableFragment._tagsForChildren != null) {
|
||||
tags.addAll(switchableFragment._tagsForChildren!);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Can be null if all fragments in group are marked as explicit.
|
||||
if (configuration != null && !rect!.isEmpty) {
|
||||
if (node == null || usedSemanticsIds.contains(node.id)) {
|
||||
node = SemanticsNode(showOnScreen: owner.showOnScreen);
|
||||
}
|
||||
usedSemanticsIds.add(node.id);
|
||||
node
|
||||
..tags = tags
|
||||
..rect = rect
|
||||
..transform = null // Will be set when compiling immediate parent node.
|
||||
..parentSemanticsClipRect = semanticsClipRect
|
||||
..parentPaintClipRect = paintClipRect;
|
||||
for (final _InterestingSemanticsFragment fragment in group) {
|
||||
if (fragment.config != null) {
|
||||
fragment.owner._semantics = node;
|
||||
}
|
||||
}
|
||||
node.updateWith(config: configuration);
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final List<_InterestingSemanticsFragment> _children = <_InterestingSemanticsFragment>[];
|
||||
|
||||
@override
|
||||
void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) {
|
||||
void compileChildren({
|
||||
Rect? parentSemanticsClipRect,
|
||||
Rect? parentPaintClipRect,
|
||||
required double elevationAdjustment,
|
||||
required List<SemanticsNode> result,
|
||||
required List<SemanticsNode> siblingNodes,
|
||||
}) {
|
||||
final Set<int> usedSemanticsIds = <int>{};
|
||||
Iterable<_InterestingSemanticsFragment> compilingFragments = _children;
|
||||
for (final List<_InterestingSemanticsFragment> siblingGroup in _siblingMergeGroups) {
|
||||
compilingFragments = compilingFragments.followedBy(siblingGroup);
|
||||
}
|
||||
if (!_isExplicit) {
|
||||
if (!_mergesToSibling) {
|
||||
owner._semantics = null;
|
||||
for (final _InterestingSemanticsFragment fragment in _children) {
|
||||
}
|
||||
_mergeSiblingGroup(
|
||||
parentSemanticsClipRect,
|
||||
parentPaintClipRect,
|
||||
siblingNodes,
|
||||
usedSemanticsIds,
|
||||
);
|
||||
for (final _InterestingSemanticsFragment fragment in compilingFragments) {
|
||||
assert(_ancestorChain.first == fragment._ancestorChain.last);
|
||||
if (fragment is _SwitchableSemanticsFragment) {
|
||||
// Cached semantics node may be part of sibling merging group prior
|
||||
// to this update. In this case, the semantics node may continue to
|
||||
// be reused in that sibling merging group.
|
||||
if (fragment._isExplicit &&
|
||||
fragment.owner._semantics != null &&
|
||||
usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
|
||||
fragment.owner._semantics = null;
|
||||
}
|
||||
}
|
||||
fragment._ancestorChain.addAll(_ancestorChain.skip(1));
|
||||
fragment.compileChildren(
|
||||
parentSemanticsClipRect: parentSemanticsClipRect,
|
||||
@ -4228,14 +4415,16 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
// its children are placed at the elevation dictated by this config.
|
||||
elevationAdjustment: elevationAdjustment + _config.elevation,
|
||||
result: result,
|
||||
siblingNodes: siblingNodes,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final _SemanticsGeometry? geometry = _needsGeometryUpdate
|
||||
? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
|
||||
: null;
|
||||
final _SemanticsGeometry? geometry = _computeSemanticsGeometry(
|
||||
parentSemanticsClipRect: parentSemanticsClipRect,
|
||||
parentPaintClipRect: parentPaintClipRect,
|
||||
);
|
||||
|
||||
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
|
||||
return; // Drop the node, it's not going to be visible.
|
||||
@ -4264,22 +4453,66 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_config.isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
final List<SemanticsNode> children = <SemanticsNode>[];
|
||||
for (final _InterestingSemanticsFragment fragment in _children) {
|
||||
_mergeSiblingGroup(
|
||||
node.parentSemanticsClipRect,
|
||||
node.parentPaintClipRect,
|
||||
siblingNodes,
|
||||
usedSemanticsIds,
|
||||
);
|
||||
for (final _InterestingSemanticsFragment fragment in compilingFragments) {
|
||||
if (fragment is _SwitchableSemanticsFragment) {
|
||||
// Cached semantics node may be part of sibling merging group prior
|
||||
// to this update. In this case, the semantics node may continue to
|
||||
// be reused in that sibling merging group.
|
||||
if (fragment._isExplicit &&
|
||||
fragment.owner._semantics != null &&
|
||||
usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
|
||||
fragment.owner._semantics = null;
|
||||
}
|
||||
}
|
||||
final List<SemanticsNode> childSiblingNodes = <SemanticsNode>[];
|
||||
fragment.compileChildren(
|
||||
parentSemanticsClipRect: node.parentSemanticsClipRect,
|
||||
parentPaintClipRect: node.parentPaintClipRect,
|
||||
elevationAdjustment: 0.0,
|
||||
result: children,
|
||||
siblingNodes: childSiblingNodes,
|
||||
);
|
||||
siblingNodes.addAll(childSiblingNodes);
|
||||
}
|
||||
|
||||
if (_config.isSemanticBoundary) {
|
||||
owner.assembleSemanticsNode(node, _config, children);
|
||||
} else {
|
||||
node.updateWith(config: _config, childrenInInversePaintOrder: children);
|
||||
}
|
||||
result.add(node);
|
||||
// Sibling node needs to attach to the parent of an explicit node.
|
||||
for (final SemanticsNode siblingNode in siblingNodes) {
|
||||
// sibling nodes are in the same coordinate of the immediate explicit node.
|
||||
// They need to share the same transform if they are going to attach to the
|
||||
// parent of the immediate explicit node.
|
||||
assert(siblingNode.transform == null);
|
||||
siblingNode
|
||||
..transform = node.transform
|
||||
..isMergedIntoParent = node.isMergedIntoParent;
|
||||
if (_tagsForChildren != null) {
|
||||
siblingNode.tags ??= <SemanticsTag>{};
|
||||
siblingNode.tags!.addAll(_tagsForChildren!);
|
||||
}
|
||||
}
|
||||
result.addAll(siblingNodes);
|
||||
siblingNodes.clear();
|
||||
}
|
||||
|
||||
_SemanticsGeometry? _computeSemanticsGeometry({
|
||||
required Rect? parentSemanticsClipRect,
|
||||
required Rect? parentPaintClipRect,
|
||||
}) {
|
||||
return _needsGeometryUpdate
|
||||
? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -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);
|
||||
|
@ -0,0 +1,259 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Semantics can merge sibling group', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
const SemanticsTag first = SemanticsTag('1');
|
||||
const SemanticsTag second = SemanticsTag('2');
|
||||
const SemanticsTag third = SemanticsTag('3');
|
||||
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
|
||||
expect(configs.length, 3);
|
||||
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
|
||||
final List<SemanticsConfiguration> sibling = <SemanticsConfiguration>[];
|
||||
// Merge first and third
|
||||
for (final SemanticsConfiguration config in configs) {
|
||||
if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
|
||||
sibling.add(config);
|
||||
} else {
|
||||
builder.markAsMergeUp(config);
|
||||
}
|
||||
}
|
||||
builder.markAsSiblingMergeGroup(sibling);
|
||||
return builder.build();
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
label: 'parent',
|
||||
child: TestConfigDelegate(
|
||||
delegate: delegate,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: '1',
|
||||
tagForChildren: first,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
// this tests that empty nodes disappear
|
||||
),
|
||||
Semantics(
|
||||
label: '2',
|
||||
tagForChildren: second,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
Semantics(
|
||||
label: '3',
|
||||
tagForChildren: third,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, hasSemantics(TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
label: 'parent\n2',
|
||||
),
|
||||
TestSemantics.rootChild(
|
||||
label: '1\n3',
|
||||
),
|
||||
],
|
||||
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
|
||||
});
|
||||
|
||||
testWidgets('Semantics can drop semantics config', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
const SemanticsTag first = SemanticsTag('1');
|
||||
const SemanticsTag second = SemanticsTag('2');
|
||||
const SemanticsTag third = SemanticsTag('3');
|
||||
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
|
||||
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
|
||||
// Merge first and third
|
||||
for (final SemanticsConfiguration config in configs) {
|
||||
if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
|
||||
continue;
|
||||
}
|
||||
builder.markAsMergeUp(config);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
label: 'parent',
|
||||
child: TestConfigDelegate(
|
||||
delegate: delegate,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: '1',
|
||||
tagForChildren: first,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
// this tests that empty nodes disappear
|
||||
),
|
||||
Semantics(
|
||||
label: '2',
|
||||
tagForChildren: second,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
Semantics(
|
||||
label: '3',
|
||||
tagForChildren: third,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, hasSemantics(TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
label: 'parent\n2',
|
||||
),
|
||||
],
|
||||
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
|
||||
});
|
||||
|
||||
testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async {
|
||||
const SemanticsTag first = SemanticsTag('1');
|
||||
const SemanticsTag second = SemanticsTag('2');
|
||||
const SemanticsTag third = SemanticsTag('3');
|
||||
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
|
||||
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
|
||||
// Marks the same one twice.
|
||||
builder.markAsMergeUp(configs.first);
|
||||
builder.markAsMergeUp(configs.first);
|
||||
return builder.build();
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
label: 'parent',
|
||||
child: TestConfigDelegate(
|
||||
delegate: delegate,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: '1',
|
||||
tagForChildren: first,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
// this tests that empty nodes disappear
|
||||
),
|
||||
Semantics(
|
||||
label: '2',
|
||||
tagForChildren: second,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
Semantics(
|
||||
label: '3',
|
||||
tagForChildren: third,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async {
|
||||
const SemanticsTag first = SemanticsTag('1');
|
||||
const SemanticsTag second = SemanticsTag('2');
|
||||
const SemanticsTag third = SemanticsTag('3');
|
||||
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
|
||||
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
|
||||
// Marks the same one twice.
|
||||
builder.markAsMergeUp(configs.first);
|
||||
builder.markAsSiblingMergeGroup(<SemanticsConfiguration>[configs.first]);
|
||||
return builder.build();
|
||||
}
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
label: 'parent',
|
||||
child: TestConfigDelegate(
|
||||
delegate: delegate,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: '1',
|
||||
tagForChildren: first,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
// this tests that empty nodes disappear
|
||||
),
|
||||
Semantics(
|
||||
label: '2',
|
||||
tagForChildren: second,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
Semantics(
|
||||
label: '3',
|
||||
tagForChildren: third,
|
||||
child: const SizedBox(width: 100, height: 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
}
|
||||
|
||||
class TestConfigDelegate extends SingleChildRenderObjectWidget {
|
||||
const TestConfigDelegate({super.key, required this.delegate, super.child});
|
||||
final ChildSemanticsConfigurationsDelegate delegate;
|
||||
|
||||
@override
|
||||
RenderTestConfigDelegate createRenderObject(BuildContext context) => RenderTestConfigDelegate(
|
||||
delegate: delegate,
|
||||
);
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderTestConfigDelegate renderObject) {
|
||||
renderObject.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderTestConfigDelegate extends RenderProxyBox {
|
||||
RenderTestConfigDelegate({
|
||||
ChildSemanticsConfigurationsDelegate? delegate,
|
||||
}) : _delegate = delegate;
|
||||
|
||||
ChildSemanticsConfigurationsDelegate? get delegate => _delegate;
|
||||
ChildSemanticsConfigurationsDelegate? _delegate;
|
||||
set delegate(ChildSemanticsConfigurationsDelegate? value) {
|
||||
if (value != _delegate) {
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
_delegate = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
config.childConfigurationsDelegate = _delegate;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user