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;
|
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
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
final BoxConstraints constraints = this.constraints;
|
final BoxConstraints constraints = this.constraints;
|
||||||
@ -1713,12 +1742,16 @@ class _AffixText extends StatelessWidget {
|
|||||||
this.text,
|
this.text,
|
||||||
this.style,
|
this.style,
|
||||||
this.child,
|
this.child,
|
||||||
|
this.semanticsSortKey,
|
||||||
|
required this.semanticsTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool labelIsFloating;
|
final bool labelIsFloating;
|
||||||
final String? text;
|
final String? text;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
final SemanticsSortKey? semanticsSortKey;
|
||||||
|
final SemanticsTag semanticsTag;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -1728,7 +1761,11 @@ class _AffixText extends StatelessWidget {
|
|||||||
duration: _kTransitionDuration,
|
duration: _kTransitionDuration,
|
||||||
curve: _kTransitionCurve,
|
curve: _kTransitionCurve,
|
||||||
opacity: labelIsFloating ? 1.0 : 0.0,
|
opacity: labelIsFloating ? 1.0 : 0.0,
|
||||||
child: child ?? (text == null ? null : Text(text!, style: style)),
|
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 _floatingLabelController;
|
||||||
late AnimationController _shakingLabelController;
|
late AnimationController _shakingLabelController;
|
||||||
final _InputBorderGap _borderGap = _InputBorderGap();
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -2218,22 +2260,42 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Widget? prefix = decoration.prefix == null && decoration.prefixText == null ? null :
|
final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
|
||||||
_AffixText(
|
final bool hasSuffix = decoration.suffix != null || decoration.suffixText != null;
|
||||||
labelIsFloating: widget._labelShouldWithdraw,
|
|
||||||
text: decoration.prefixText,
|
|
||||||
style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
|
|
||||||
child: decoration.prefix,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Widget? suffix = decoration.suffix == null && decoration.suffixText == null ? null :
|
Widget? input = widget.child;
|
||||||
_AffixText(
|
// If at least two out of the three are visible, it needs semantics sort
|
||||||
labelIsFloating: widget._labelShouldWithdraw,
|
// order.
|
||||||
text: decoration.suffixText,
|
final bool needsSemanticsSortOrder = widget._labelShouldWithdraw && (input != null ? (hasPrefix || hasSuffix) : (hasPrefix && hasSuffix));
|
||||||
style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
|
|
||||||
child: decoration.suffix,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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 = 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 bool decorationIsDense = decoration.isDense ?? false;
|
||||||
final double iconSize = decorationIsDense ? 18.0 : 24.0;
|
final double iconSize = decorationIsDense ? 18.0 : 24.0;
|
||||||
@ -2272,7 +2334,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
|||||||
color: _getPrefixIconColor(themeData, defaults),
|
color: _getPrefixIconColor(themeData, defaults),
|
||||||
size: iconSize,
|
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),
|
color: _getSuffixIconColor(themeData, defaults),
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
child: decoration.suffixIcon!,
|
child: Semantics(
|
||||||
|
child: decoration.suffixIcon,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -2374,7 +2440,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
|||||||
isDense: decoration.isDense,
|
isDense: decoration.isDense,
|
||||||
visualDensity: themeData.visualDensity,
|
visualDensity: themeData.visualDensity,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
input: widget.child,
|
input: input,
|
||||||
label: label,
|
label: label,
|
||||||
hint: hint,
|
hint: hint,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
|
@ -3100,6 +3100,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
if (_cachedSemanticsConfiguration == null) {
|
if (_cachedSemanticsConfiguration == null) {
|
||||||
_cachedSemanticsConfiguration = SemanticsConfiguration();
|
_cachedSemanticsConfiguration = SemanticsConfiguration();
|
||||||
describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
|
describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
|
||||||
|
assert(
|
||||||
|
!_cachedSemanticsConfiguration!.explicitChildNodes || _cachedSemanticsConfiguration!.childConfigurationsDelegate == null,
|
||||||
|
'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _cachedSemanticsConfiguration!;
|
return _cachedSemanticsConfiguration!;
|
||||||
}
|
}
|
||||||
@ -3161,7 +3165,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
|
|
||||||
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
|
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
|
||||||
_cachedSemanticsConfiguration = null;
|
_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;
|
RenderObject node = this;
|
||||||
|
|
||||||
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
|
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
|
||||||
@ -3213,11 +3223,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
assert(fragment is _InterestingSemanticsFragment);
|
assert(fragment is _InterestingSemanticsFragment);
|
||||||
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
|
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
|
||||||
final List<SemanticsNode> result = <SemanticsNode>[];
|
final List<SemanticsNode> result = <SemanticsNode>[];
|
||||||
|
final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
|
||||||
interestingFragment.compileChildren(
|
interestingFragment.compileChildren(
|
||||||
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
|
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
|
||||||
parentPaintClipRect: _semantics?.parentPaintClipRect,
|
parentPaintClipRect: _semantics?.parentPaintClipRect,
|
||||||
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
|
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
|
||||||
result: result,
|
result: result,
|
||||||
|
siblingNodes: siblingNodes,
|
||||||
);
|
);
|
||||||
final SemanticsNode node = result.single;
|
final SemanticsNode node = result.single;
|
||||||
// Fragment only wants to add this node's SemanticsNode to the parent.
|
// 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;
|
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
|
||||||
|
|
||||||
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
|
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
|
||||||
final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
|
|
||||||
final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = <_InterestingSemanticsFragment>{};
|
|
||||||
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
|
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) {
|
visitChildrenForSemantics((RenderObject renderChild) {
|
||||||
assert(!_needsLayout);
|
assert(!_needsLayout);
|
||||||
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
|
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
|
||||||
mergeIntoParent: childrenMergeIntoParent,
|
mergeIntoParent: childrenMergeIntoParent,
|
||||||
);
|
);
|
||||||
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
|
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
|
||||||
fragments.clear();
|
childConfigurations.clear();
|
||||||
toBeMarkedExplicit.clear();
|
mergeUpFragments.clear();
|
||||||
|
siblingMergeFragmentGroups.clear();
|
||||||
if (!config.isSemanticBoundary) {
|
if (!config.isSemanticBoundary) {
|
||||||
dropSemanticsOfPreviousSiblings = true;
|
dropSemanticsOfPreviousSiblings = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Figure out which child fragments are to be made explicit.
|
for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
|
||||||
for (final _InterestingSemanticsFragment fragment in parentFragment.interestingFragments) {
|
|
||||||
fragments.add(fragment);
|
|
||||||
fragment.addAncestor(this);
|
fragment.addAncestor(this);
|
||||||
fragment.addTags(config.tagsForChildren);
|
fragment.addTags(config.tagsForChildren);
|
||||||
if (config.explicitChildNodes || parent is! RenderObject) {
|
if (hasChildConfigurationsDelegate && fragment.config != null) {
|
||||||
fragment.markAsExplicit();
|
// This fragment need to go through delegate to determine whether it
|
||||||
continue;
|
// merge up or not.
|
||||||
|
childConfigurations.add(fragment.config!);
|
||||||
|
configToFragment[fragment.config!] = fragment;
|
||||||
|
} else {
|
||||||
|
mergeUpFragments.add(fragment);
|
||||||
}
|
}
|
||||||
if (!fragment.hasConfigForParent || producesForkingFragment) {
|
}
|
||||||
continue;
|
if (parentFragment is _ContainerSemanticsFragment) {
|
||||||
}
|
// Container fragments needs to propagate sibling merge group to be
|
||||||
if (!config.isCompatibleWith(fragment.config)) {
|
// compiled by _SwitchableSemanticsFragment.
|
||||||
toBeMarkedExplicit.add(fragment);
|
for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
|
||||||
}
|
for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
|
||||||
final int siblingLength = fragments.length - 1;
|
siblingMergingFragment.addAncestor(this);
|
||||||
for (int i = 0; i < siblingLength; i += 1) {
|
siblingMergingFragment.addTags(config.tagsForChildren);
|
||||||
final _InterestingSemanticsFragment siblingFragment = fragments[i];
|
|
||||||
if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
|
|
||||||
toBeMarkedExplicit.add(fragment);
|
|
||||||
toBeMarkedExplicit.add(siblingFragment);
|
|
||||||
}
|
}
|
||||||
|
siblingMergeFragmentGroups.add(siblingMergeGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (final _InterestingSemanticsFragment fragment in toBeMarkedExplicit) {
|
assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
|
||||||
fragment.markAsExplicit();
|
|
||||||
|
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;
|
_needsSemanticsUpdate = false;
|
||||||
|
|
||||||
_SemanticsFragment result;
|
final _SemanticsFragment result;
|
||||||
if (parent is! RenderObject) {
|
if (parent is! RenderObject) {
|
||||||
assert(!config.hasBeenAnnotated);
|
assert(!config.hasBeenAnnotated);
|
||||||
assert(!mergeIntoParent);
|
assert(!mergeIntoParent);
|
||||||
|
assert(siblingMergeFragmentGroups.isEmpty);
|
||||||
|
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
|
||||||
|
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
|
||||||
result = _RootSemanticsFragment(
|
result = _RootSemanticsFragment(
|
||||||
owner: this,
|
owner: this,
|
||||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||||
);
|
);
|
||||||
} else if (producesForkingFragment) {
|
} else if (producesForkingFragment) {
|
||||||
result = _ContainerSemanticsFragment(
|
result = _ContainerSemanticsFragment(
|
||||||
|
siblingMergeGroups: siblingMergeFragmentGroups,
|
||||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
|
||||||
|
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
|
||||||
result = _SwitchableSemanticsFragment(
|
result = _SwitchableSemanticsFragment(
|
||||||
config: config,
|
config: config,
|
||||||
mergeIntoParent: mergeIntoParent,
|
mergeIntoParent: mergeIntoParent,
|
||||||
|
siblingMergeGroups: siblingMergeFragmentGroups,
|
||||||
owner: this,
|
owner: this,
|
||||||
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
|
||||||
);
|
);
|
||||||
@ -3307,12 +3343,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
|||||||
fragment.markAsExplicit();
|
fragment.markAsExplicit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result.addAll(mergeUpFragments);
|
||||||
result.addAll(fragments);
|
|
||||||
|
|
||||||
return result;
|
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.
|
/// Called when collecting the semantics of this node.
|
||||||
///
|
///
|
||||||
/// The implementation has to return the children in paint order skipping all
|
/// 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
|
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic
|
||||||
/// information of multiple [_InterestingSemanticsFragment] to a parent.
|
/// information of multiple [_InterestingSemanticsFragment] to a parent.
|
||||||
abstract class _SemanticsFragment {
|
abstract class _SemanticsFragment {
|
||||||
_SemanticsFragment({ required this.dropsSemanticsOfPreviousSiblings })
|
_SemanticsFragment({
|
||||||
: assert (dropsSemanticsOfPreviousSiblings != null);
|
required this.dropsSemanticsOfPreviousSiblings,
|
||||||
|
}) : assert (dropsSemanticsOfPreviousSiblings != null);
|
||||||
|
|
||||||
/// Incorporate the fragments of children into this fragment.
|
/// Incorporate the fragments of children into this fragment.
|
||||||
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
|
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
|
||||||
@ -4002,25 +4061,29 @@ abstract class _SemanticsFragment {
|
|||||||
|
|
||||||
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
|
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
|
||||||
/// information that this fragment wants to add to the parent.
|
/// 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
|
/// A container used when a [RenderObject] wants to add multiple independent
|
||||||
/// [_InterestingSemanticsFragment] to its parent.
|
/// [_InterestingSemanticsFragment] to its parent.
|
||||||
///
|
///
|
||||||
/// The [_InterestingSemanticsFragment] to be added to the parent can be
|
/// The [_InterestingSemanticsFragment] to be added to the parent can be
|
||||||
/// obtained via [interestingFragments].
|
/// obtained via [mergeUpFragments].
|
||||||
class _ContainerSemanticsFragment extends _SemanticsFragment {
|
class _ContainerSemanticsFragment extends _SemanticsFragment {
|
||||||
|
_ContainerSemanticsFragment({
|
||||||
|
required super.dropsSemanticsOfPreviousSiblings,
|
||||||
|
required this.siblingMergeGroups,
|
||||||
|
});
|
||||||
|
|
||||||
_ContainerSemanticsFragment({ required super.dropsSemanticsOfPreviousSiblings });
|
final List<List<_InterestingSemanticsFragment>> siblingMergeGroups;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
|
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
|
||||||
interestingFragments.addAll(fragments);
|
mergeUpFragments.addAll(fragments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[];
|
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [_SemanticsFragment] that describes which concrete semantic information
|
/// A [_SemanticsFragment] that describes which concrete semantic information
|
||||||
@ -4057,6 +4120,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
|||||||
required Rect? parentPaintClipRect,
|
required Rect? parentPaintClipRect,
|
||||||
required double elevationAdjustment,
|
required double elevationAdjustment,
|
||||||
required List<SemanticsNode> result,
|
required List<SemanticsNode> result,
|
||||||
|
required List<SemanticsNode> siblingNodes,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The [SemanticsConfiguration] the child wants to merge into the parent's
|
/// 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;
|
bool get hasConfigForParent => config != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this];
|
List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
|
||||||
|
|
||||||
Set<SemanticsTag>? _tagsForChildren;
|
Set<SemanticsTag>? _tagsForChildren;
|
||||||
|
|
||||||
@ -4124,7 +4188,13 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@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(_tagsForChildren == null || _tagsForChildren!.isEmpty);
|
||||||
assert(parentSemanticsClipRect == null);
|
assert(parentSemanticsClipRect == null);
|
||||||
assert(parentPaintClipRect == null);
|
assert(parentPaintClipRect == null);
|
||||||
@ -4150,8 +4220,11 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
parentPaintClipRect: parentPaintClipRect,
|
parentPaintClipRect: parentPaintClipRect,
|
||||||
elevationAdjustment: 0.0,
|
elevationAdjustment: 0.0,
|
||||||
result: children,
|
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);
|
node.updateWith(config: null, childrenInInversePaintOrder: children);
|
||||||
|
|
||||||
// The root node is the only semantics node allowed to be invisible. This
|
// The root node is the only semantics node allowed to be invisible. This
|
||||||
@ -4201,9 +4274,11 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
_SwitchableSemanticsFragment({
|
_SwitchableSemanticsFragment({
|
||||||
required bool mergeIntoParent,
|
required bool mergeIntoParent,
|
||||||
required SemanticsConfiguration config,
|
required SemanticsConfiguration config,
|
||||||
|
required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
|
||||||
required super.owner,
|
required super.owner,
|
||||||
required super.dropsSemanticsOfPreviousSiblings,
|
required super.dropsSemanticsOfPreviousSiblings,
|
||||||
}) : _mergeIntoParent = mergeIntoParent,
|
}) : _siblingMergeGroups = siblingMergeGroups,
|
||||||
|
_mergeIntoParent = mergeIntoParent,
|
||||||
_config = config,
|
_config = config,
|
||||||
assert(mergeIntoParent != null),
|
assert(mergeIntoParent != null),
|
||||||
assert(config != null);
|
assert(config != null);
|
||||||
@ -4211,14 +4286,126 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
final bool _mergeIntoParent;
|
final bool _mergeIntoParent;
|
||||||
SemanticsConfiguration _config;
|
SemanticsConfiguration _config;
|
||||||
bool _isConfigWritable = false;
|
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>[];
|
final List<_InterestingSemanticsFragment> _children = <_InterestingSemanticsFragment>[];
|
||||||
|
|
||||||
@override
|
@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 (!_isExplicit) {
|
||||||
owner._semantics = null;
|
if (!_mergesToSibling) {
|
||||||
for (final _InterestingSemanticsFragment fragment in _children) {
|
owner._semantics = null;
|
||||||
|
}
|
||||||
|
_mergeSiblingGroup(
|
||||||
|
parentSemanticsClipRect,
|
||||||
|
parentPaintClipRect,
|
||||||
|
siblingNodes,
|
||||||
|
usedSemanticsIds,
|
||||||
|
);
|
||||||
|
for (final _InterestingSemanticsFragment fragment in compilingFragments) {
|
||||||
assert(_ancestorChain.first == fragment._ancestorChain.last);
|
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._ancestorChain.addAll(_ancestorChain.skip(1));
|
||||||
fragment.compileChildren(
|
fragment.compileChildren(
|
||||||
parentSemanticsClipRect: parentSemanticsClipRect,
|
parentSemanticsClipRect: parentSemanticsClipRect,
|
||||||
@ -4228,14 +4415,16 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
// its children are placed at the elevation dictated by this config.
|
// its children are placed at the elevation dictated by this config.
|
||||||
elevationAdjustment: elevationAdjustment + _config.elevation,
|
elevationAdjustment: elevationAdjustment + _config.elevation,
|
||||||
result: result,
|
result: result,
|
||||||
|
siblingNodes: siblingNodes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final _SemanticsGeometry? geometry = _needsGeometryUpdate
|
final _SemanticsGeometry? geometry = _computeSemanticsGeometry(
|
||||||
? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
|
parentSemanticsClipRect: parentSemanticsClipRect,
|
||||||
: null;
|
parentPaintClipRect: parentPaintClipRect,
|
||||||
|
);
|
||||||
|
|
||||||
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
|
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
|
||||||
return; // Drop the node, it's not going to be visible.
|
return; // Drop the node, it's not going to be visible.
|
||||||
@ -4264,22 +4453,66 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
_config.isHidden = true;
|
_config.isHidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SemanticsNode> children = <SemanticsNode>[];
|
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(
|
fragment.compileChildren(
|
||||||
parentSemanticsClipRect: node.parentSemanticsClipRect,
|
parentSemanticsClipRect: node.parentSemanticsClipRect,
|
||||||
parentPaintClipRect: node.parentPaintClipRect,
|
parentPaintClipRect: node.parentPaintClipRect,
|
||||||
elevationAdjustment: 0.0,
|
elevationAdjustment: 0.0,
|
||||||
result: children,
|
result: children,
|
||||||
|
siblingNodes: childSiblingNodes,
|
||||||
);
|
);
|
||||||
|
siblingNodes.addAll(childSiblingNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.isSemanticBoundary) {
|
if (_config.isSemanticBoundary) {
|
||||||
owner.assembleSemanticsNode(node, _config, children);
|
owner.assembleSemanticsNode(node, _config, children);
|
||||||
} else {
|
} else {
|
||||||
node.updateWith(config: _config, childrenInInversePaintOrder: children);
|
node.updateWith(config: _config, childrenInInversePaintOrder: children);
|
||||||
}
|
}
|
||||||
result.add(node);
|
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
|
@override
|
||||||
|
@ -6,6 +6,7 @@ import 'dart:math' as math;
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
|
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
|
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -53,6 +54,20 @@ typedef SemanticsActionHandler = void Function(Object? args);
|
|||||||
/// Used by [SemanticsOwner.onSemanticsUpdate].
|
/// Used by [SemanticsOwner.onSemanticsUpdate].
|
||||||
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
|
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].
|
/// A tag for a [SemanticsNode].
|
||||||
///
|
///
|
||||||
/// Tags can be interpreted by the parent of a [SemanticsNode]
|
/// Tags can be interpreted by the parent of a [SemanticsNode]
|
||||||
@ -85,6 +100,89 @@ class SemanticsTag {
|
|||||||
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
|
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.
|
/// An identifier of a custom semantics action.
|
||||||
///
|
///
|
||||||
/// Custom semantics actions can be provided to make complex user
|
/// Custom semantics actions can be provided to make complex user
|
||||||
@ -3724,6 +3822,25 @@ class SemanticsConfiguration {
|
|||||||
_onDidLoseAccessibilityFocus = value;
|
_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
|
/// Returns the action handler registered for [action] or null if none was
|
||||||
/// registered.
|
/// registered.
|
||||||
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
|
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
|
||||||
@ -4448,6 +4565,11 @@ class SemanticsConfiguration {
|
|||||||
/// * [addTagForChildren] to add a tag and for more information about their
|
/// * [addTagForChildren] to add a tag and for more information about their
|
||||||
/// usage.
|
/// usage.
|
||||||
Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren;
|
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;
|
Set<SemanticsTag>? _tagsForChildren;
|
||||||
|
|
||||||
/// Specifies a [SemanticsTag] that this configuration wants to apply to all
|
/// Specifies a [SemanticsTag] that this configuration wants to apply to all
|
||||||
|
@ -4375,6 +4375,47 @@ void main() {
|
|||||||
expect(prefixText.style, prefixStyle);
|
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 {
|
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
|
||||||
final TextStyle suffixStyle = TextStyle(
|
final TextStyle suffixStyle = TextStyle(
|
||||||
color: Colors.pink[500],
|
color: Colors.pink[500],
|
||||||
|
@ -1429,6 +1429,51 @@ void main() {
|
|||||||
handle.dispose();
|
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 {
|
testWidgets('Scroll inertia cancel event', (WidgetTester tester) async {
|
||||||
await pumpTest(tester, null);
|
await pumpTest(tester, null);
|
||||||
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
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