Unverified Commit fb7a5937 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Basic scroll semantics support (#21764)

parent 59f1f1b6
......@@ -30,6 +30,8 @@ const List<String> coolColorNames = <String>[
'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre',
];
const int _kChildCount = 50;
class CupertinoNavigationDemo extends StatelessWidget {
CupertinoNavigationDemo()
: colorItems = List<Color>.generate(50, (int index) {
......@@ -146,6 +148,7 @@ class CupertinoDemoTab1 extends StatelessWidget {
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
semanticChildCount: _kChildCount,
slivers: <Widget>[
CupertinoSliverNavigationBar(
trailing: trailingButtons,
......@@ -163,12 +166,12 @@ class CupertinoDemoTab1 extends StatelessWidget {
(BuildContext context, int index) {
return Tab1RowItem(
index: index,
lastItem: index == 49,
lastItem: index == _kChildCount - 1,
color: colorItems[index],
colorName: colorNameItems[index],
);
},
childCount: 50,
childCount: _kChildCount,
),
),
),
......
......@@ -91,6 +91,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
},
),
body: CustomScrollView(
semanticChildCount: widget.recipes.length,
slivers: <Widget>[
_buildAppBar(context, statusBarHeight),
_buildBody(context, statusBarHeight),
......
......@@ -4330,6 +4330,49 @@ class RenderExcludeSemantics extends RenderProxyBox {
}
}
/// A render objects that annotates semantics with an index.
///
/// Certain widgets will automatically provide a child index for building
/// semantics. For example, the [ScrollView] uses the index of the first
/// visible child semantics node to determine the
/// [SemanticsConfiguration.scrollIndex].
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
class RenderIndexedSemantics extends RenderProxyBox {
/// Creates a render object that annotates the child semantics with an index.
RenderIndexedSemantics({
RenderBox child,
@required int index,
}) : assert(index != null),
_index = index,
super(child);
/// The index used to annotated child semantics.
int get index => _index;
int _index;
set index(int value) {
if (value == index)
return;
_index = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
config.indexInParent = index;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<int>('index', index));
}
}
/// Provides an anchor for a [RenderFollowerLayer].
///
/// See also:
......
......@@ -188,6 +188,8 @@ class SemanticsData extends Diagnosticable {
@required this.textDirection,
@required this.rect,
@required this.textSelection,
@required this.scrollIndex,
@required this.scrollChildCount,
@required this.scrollPosition,
@required this.scrollExtentMax,
@required this.scrollExtentMin,
......@@ -249,6 +251,15 @@ class SemanticsData extends Diagnosticable {
/// if this node represents a text field.
final TextSelection textSelection;
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
/// null.
final int scrollChildCount;
/// The index of the first visible semantic child of a scroll node.
final int scrollIndex;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
......@@ -343,6 +354,8 @@ class SemanticsData extends Diagnosticable {
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid == true)
properties.add(MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]'));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
......@@ -363,6 +376,8 @@ class SemanticsData extends Diagnosticable {
&& typedOther.textDirection == textDirection
&& typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
&& typedOther.scrollChildCount == scrollChildCount
&& typedOther.scrollIndex == scrollIndex
&& typedOther.textSelection == textSelection
&& typedOther.scrollPosition == scrollPosition
&& typedOther.scrollExtentMax == scrollExtentMax
......@@ -385,6 +400,8 @@ class SemanticsData extends Diagnosticable {
rect,
tags,
textSelection,
scrollChildCount,
scrollIndex,
scrollPosition,
scrollExtentMax,
scrollExtentMin,
......@@ -1125,6 +1142,14 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// If this rect is null [parentSemanticsClipRect] also has to be null.
Rect parentPaintClipRect;
/// The index of this node within the parent's list of semantic children.
///
/// This includes all semantic nodes, not just those currently in the
/// child list. For example, if a scrollable has five children but the first
/// two are not visible (and thus not included in the list of children), then
/// the index of the last node will still be 4.
int indexInParent;
/// Whether the node is invisible.
///
/// A node whose [rect] is outside of the bounds of the screen and hence not
......@@ -1394,6 +1419,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollExtentMax != config._scrollExtentMax ||
_scrollExtentMin != config._scrollExtentMin ||
_actionsAsBits != config._actionsAsBits ||
indexInParent != config.indexInParent ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
......@@ -1415,7 +1441,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
int _flags = _kEmptyConfig._flags;
bool _hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;
/// Whether this node currently has a given [SemanticsFlag].
bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;
/// A textual description of this node.
///
......@@ -1479,6 +1506,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextSelection get textSelection => _textSelection;
TextSelection _textSelection;
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
/// null.
int get scrollChildCount => _scrollChildCount;
int _scrollChildCount;
/// The index of the first visible semantic child of a scroll node.
int get scrollIndex => _scrollIndex;
int _scrollIndex;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
......@@ -1553,6 +1591,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollExtentMax = config._scrollExtentMax;
_scrollExtentMin = config._scrollExtentMin;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_scrollChildCount = config.scrollChildCount;
_scrollIndex = config.scrollIndex;
indexInParent = config.indexInParent;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert(
......@@ -1582,6 +1623,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextDirection textDirection = _textDirection;
Set<SemanticsTag> mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags);
TextSelection textSelection = _textSelection;
int scrollChildCount = _scrollChildCount;
int scrollIndex = _scrollIndex;
double scrollPosition = _scrollPosition;
double scrollExtentMax = _scrollExtentMax;
double scrollExtentMin = _scrollExtentMin;
......@@ -1612,6 +1655,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
actions |= node._actionsAsBits;
textDirection ??= node._textDirection;
textSelection ??= node._textSelection;
scrollChildCount ??= node._scrollChildCount;
scrollIndex ??= node._scrollIndex;
scrollPosition ??= node._scrollPosition;
scrollExtentMax ??= node._scrollExtentMax;
scrollExtentMin ??= node._scrollExtentMin;
......@@ -1674,6 +1719,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
transform: transform,
tags: mergedTags,
textSelection: textSelection,
scrollChildCount: scrollChildCount,
scrollIndex: scrollIndex,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
......@@ -1732,6 +1779,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
scrollChildren: data.scrollChildCount != null ? data.scrollChildCount : 0,
scrollIndex: data.scrollIndex != null ? data.scrollIndex : 0 ,
scrollPosition: data.scrollPosition != null ? data.scrollPosition : double.nan,
scrollExtentMax: data.scrollExtentMax != null ? data.scrollExtentMax : double.nan,
scrollExtentMin: data.scrollExtentMin != null ? data.scrollExtentMin : double.nan,
......@@ -1855,10 +1904,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
.toList();
properties.add(IterableProperty<String>('actions', actions, ifEmpty: null));
properties.add(IterableProperty<String>('customActions', customSemanticsActions, ifEmpty: null));
final List<String> flags = SemanticsFlag.values.values.where((SemanticsFlag flag) => _hasFlag(flag)).map((SemanticsFlag flag) => flag.toString().substring('SemanticsFlag.'.length)).toList();
final List<String> flags = SemanticsFlag.values.values.where((SemanticsFlag flag) => hasFlag(flag)).map((SemanticsFlag flag) => flag.toString().substring('SemanticsFlag.'.length)).toList();
properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(FlagProperty('isHidden', value: _hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
properties.add(StringProperty('label', _label, defaultValue: ''));
properties.add(StringProperty('value', _value, defaultValue: ''));
properties.add(StringProperty('increasedValue', _increasedValue, defaultValue: ''));
......@@ -1868,6 +1917,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid == true)
properties.add(MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
......@@ -2889,6 +2940,44 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// The index of this node within the parent's list of semantic children.
///
/// This includes all semantic nodes, not just those currently in the
/// child list. For example, if a scrollable has five children but the first
/// two are not visible (and thus not included in the list of children), then
/// the index of the last node will still be 4.
int get indexInParent => _indexInParent;
int _indexInParent;
set indexInParent(int value) {
_indexInParent = value;
_hasBeenAnnotated = true;
}
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
/// null.
int get scrollChildCount => _scrollChildCount;
int _scrollChildCount;
set scrollChildCount(int value) {
if (value == scrollChildCount)
return;
_scrollChildCount = value;
_hasBeenAnnotated = true;
}
/// The index of the first visible scrollable child that contributes to
/// semantics.
int get scrollIndex => _scrollIndex;
int _scrollIndex;
set scrollIndex(int value) {
if (value == scrollIndex)
return;
_scrollIndex = value;
_hasBeenAnnotated = true;
}
/// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity.
///
......@@ -3358,6 +3447,9 @@ class SemanticsConfiguration {
_scrollExtentMax ??= other._scrollExtentMax;
_scrollExtentMin ??= other._scrollExtentMin;
_hintOverrides ??= other._hintOverrides;
_indexInParent ??= other.indexInParent;
_scrollIndex ??= other._scrollIndex;
_scrollChildCount ??= other._scrollChildCount;
textDirection ??= other.textDirection;
_sortKey ??= other._sortKey;
......@@ -3406,6 +3498,9 @@ class SemanticsConfiguration {
.._scrollExtentMax = _scrollExtentMax
.._scrollExtentMin = _scrollExtentMin
.._actionsAsBits = _actionsAsBits
.._indexInParent = indexInParent
.._scrollIndex = _scrollIndex
.._scrollChildCount = _scrollChildCount
.._actions.addAll(_actions)
.._customSemanticsActions.addAll(_customSemanticsActions);
}
......
......@@ -5553,6 +5553,64 @@ class ExcludeSemantics extends SingleChildRenderObjectWidget {
}
}
/// A widget that annotates the child semantics with an index.
///
/// Semantic indexes are used by TalkBack/Voiceover to make announcements about
/// the current scroll state. Certain widgets like the [ListView] will
/// automatically provide a child index for building semantics. A user may wish
/// to manually provide semanitc indexes if not all child of the scrollable
/// contribute semantics.
///
/// ## Sample code
///
/// The example below handles spacers in a scrollable that don't contribute
/// semantics. The automatic indexes would give the spaces a semantic index,
/// causing scroll announcements to erroneously state that there are four items
/// visible.
///
/// ```dart
/// ListView(
/// addSemanticIndexes: false,
/// semanticChildCount: 2,
/// children: const <Widget>[
/// IndexedSemantics(index: 0, child: Text('First')),
/// Spacer(),
/// IndexedSemantics(index: 1, child: Text('Second')),
/// Spacer(),
/// ],
/// )
/// ```
///
/// See also:
/// * [CustomScrollView], for an explaination of index semantics.
class IndexedSemantics extends SingleChildRenderObjectWidget {
/// Creates a widget that annotated the first child semantics node with an index.
///
/// [index] must not be null.
const IndexedSemantics({
Key key,
@required this.index,
Widget child,
}) : assert(index != null),
super(key: key, child: child);
/// The index used to annotate the first child semantics node.
final int index;
@override
RenderIndexedSemantics createRenderObject(BuildContext context) => RenderIndexedSemantics(index: index);
@override
void updateRenderObject(BuildContext context, RenderIndexedSemantics renderObject) {
renderObject.index = index;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<int>('index', index));
}
}
/// A widget that builds its child.
///
/// Useful for attaching a key to an existing widget.
......
......@@ -60,6 +60,7 @@ abstract class ScrollView extends StatelessWidget {
ScrollPhysics physics,
this.shrinkWrap = false,
this.cacheExtent,
this.semanticChildCount,
}) : assert(reverse != null),
assert(shrinkWrap != null),
assert(!(controller != null && primary == true),
......@@ -172,6 +173,21 @@ abstract class ScrollView extends StatelessWidget {
/// {@macro flutter.rendering.viewport.cacheExtent}
final double cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [new ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
final int semanticChildCount;
/// Returns the [AxisDirection] in which the scroll view scrolls.
///
/// Combines the [scrollDirection] with the [reverse] boolean to obtain the
......@@ -234,6 +250,7 @@ abstract class ScrollView extends StatelessWidget {
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
......@@ -317,6 +334,40 @@ abstract class ScrollView extends StatelessWidget {
/// )
/// ```
///
/// ## Accessibility
///
/// A [CustomScrollView] can allow Talkback/VoiceOver to make announcements
/// to the user when the scroll state changes. For example, on Android an
/// announcement might be read as "showing items 1 to 10 of 23". To produce
/// this announcment, the scroll view needs three pieces of information:
///
/// * The first visible child index.
/// * The total number of children.
/// * The total number of visible children.
///
/// The last value can be computed exactly by the framework, however the first
/// two must be provided. Most of the higher-level scrollable widgets provide
/// this information automatically. For example, [ListView] provides each child
/// widget with a semantic index automatically and sets the semantic child
/// count to the length of the list.
///
/// To determine visible indexes, the scroll view needs a way to associate the
/// generated semantics of each scrollable item with a semantic index. This can
/// be done by wrapping the child widgets in an [IndexedSemantics].
///
/// This semantic index is not necesarily the same as the index of the widget
/// in the scrollable, because some widgets may not contribute semantic
/// information. Consider a [new ListView.separated()], every other widget is a
/// divider with no semantic information. In this case, only odd numbered
/// widgets have a semantic index (equal to the index ~/ 2). Furthermore, the
/// total number of children in this example would be half the number of
/// widgets. Note that [new ListView.separated()] handles this automatically
/// and is only used here as an example.
///
/// The total number of visible children can be provided by the constructor
/// parameter `semanticChildCount`. This should always be the same as the
/// number of widgets wrapped in [IndexedSemantics].
///
/// See also:
///
/// * [SliverList], which is a sliver that displays linear list of children.
......@@ -329,6 +380,8 @@ abstract class ScrollView extends StatelessWidget {
/// and float as the scroll view scrolls.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
/// * [IndexedSemantics], which allows annotating child lists with an index
/// for scroll announcements.
class CustomScrollView extends ScrollView {
/// Creates a [ScrollView] that creates custom scroll effects using slivers.
///
......@@ -343,6 +396,7 @@ class CustomScrollView extends ScrollView {
bool shrinkWrap = false,
double cacheExtent,
this.slivers = const <Widget>[],
int semanticChildCount,
}) : super(
key: key,
scrollDirection: scrollDirection,
......@@ -352,6 +406,7 @@ class CustomScrollView extends ScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
);
/// The slivers to place inside the viewport.
......@@ -383,6 +438,7 @@ abstract class BoxScrollView extends ScrollView {
bool shrinkWrap = false,
this.padding,
double cacheExtent,
int semanticChildCount,
}) : super(
key: key,
scrollDirection: scrollDirection,
......@@ -392,6 +448,7 @@ abstract class BoxScrollView extends ScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
);
/// The amount of space by which to inset the children.
......@@ -672,12 +729,15 @@ class ListView extends BoxScrollView {
this.itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super(
key: key,
scrollDirection: scrollDirection,
......@@ -688,6 +748,7 @@ class ListView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
);
/// Creates a scrollable, linear array of widgets that are created on demand.
......@@ -728,12 +789,15 @@ class ListView extends BoxScrollView {
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super(
key: key,
scrollDirection: scrollDirection,
......@@ -743,7 +807,8 @@ class ListView extends BoxScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
);
/// Creates a fixed-length scrollable linear array of list "items" separated
......@@ -804,6 +869,7 @@ class ListView extends BoxScrollView {
@required int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
}) : assert(itemBuilder != null),
assert(separatorBuilder != null),
......@@ -816,9 +882,13 @@ class ListView extends BoxScrollView {
? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex);
},
childCount: math.max(0, itemCount * 2 - 1),
childCount: _computeSemanticChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
semanticIndexCallback: (Widget _, int index) {
return index.isEven ? index ~/ 2 : null;
}
), super(
key: key,
scrollDirection: scrollDirection,
......@@ -828,7 +898,8 @@ class ListView extends BoxScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent
cacheExtent: cacheExtent,
semanticChildCount: _computeSemanticChildCount(itemCount),
);
/// Creates a scrollable, linear array of widgets with a custom child model.
......@@ -847,6 +918,7 @@ class ListView extends BoxScrollView {
this.itemExtent,
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
}) : assert(childrenDelegate != null),
super(
key: key,
......@@ -858,6 +930,7 @@ class ListView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
);
/// If non-null, forces the children to have the given extent in the scroll
......@@ -893,6 +966,11 @@ class ListView extends BoxScrollView {
super.debugFillProperties(properties);
properties.add(DoubleProperty('itemExtent', itemExtent, defaultValue: null));
}
// Helper method to compute the semantic child count for the separated constructor.
static int _computeSemanticChildCount(int itemCount) {
return math.max(0, itemCount * 2 - 1);
}
}
/// A scrollable, 2D array of widgets.
......@@ -1047,13 +1125,16 @@ class GridView extends BoxScrollView {
@required this.gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
key: key,
......@@ -1065,6 +1146,7 @@ class GridView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
);
/// Creates a scrollable, 2D array of widgets that are created on demand.
......@@ -1100,13 +1182,16 @@ class GridView extends BoxScrollView {
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
key: key,
......@@ -1118,6 +1203,7 @@ class GridView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
);
/// Creates a scrollable, 2D array of widgets with both a custom
......@@ -1139,6 +1225,7 @@ class GridView extends BoxScrollView {
@required this.gridDelegate,
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
}) : assert(gridDelegate != null),
assert(childrenDelegate != null),
super(
......@@ -1151,6 +1238,7 @@ class GridView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
);
/// Creates a scrollable, 2D array of widgets with a fixed number of tiles in
......@@ -1182,8 +1270,10 @@ class GridView extends BoxScrollView {
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
......@@ -1194,6 +1284,7 @@ class GridView extends BoxScrollView {
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super(
key: key,
scrollDirection: scrollDirection,
......@@ -1204,6 +1295,7 @@ class GridView extends BoxScrollView {
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
);
/// Creates a scrollable, 2D array of widgets with tiles that each have a
......@@ -1235,7 +1327,9 @@ class GridView extends BoxScrollView {
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
......@@ -1246,6 +1340,7 @@ class GridView extends BoxScrollView {
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super(
key: key,
scrollDirection: scrollDirection,
......@@ -1255,6 +1350,7 @@ class GridView extends BoxScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
semanticChildCount: semanticChildCount ?? children.length,
);
/// A delegate that controls the layout of the children within the [GridView].
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
......@@ -79,6 +80,7 @@ class Scrollable extends StatefulWidget {
this.physics,
@required this.viewportBuilder,
this.excludeFromSemantics = false,
this.semanticChildCount,
}) : assert(axisDirection != null),
assert(viewportBuilder != null),
assert(excludeFromSemantics != null),
......@@ -161,6 +163,23 @@ class Scrollable extends StatefulWidget {
/// exclusion.
final bool excludeFromSemantics;
/// The number of children that will contribute semantic information.
///
/// The value will be null if the number of children is unknown or unbounded.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [new ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided.
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
final int semanticChildCount;
/// The axis along which the scroll view scrolls.
///
/// Determined by the [axisDirection].
......@@ -509,6 +528,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
child: result,
position: position,
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? false,
semanticChildCount: widget.semanticChildCount,
);
}
......@@ -541,17 +561,20 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
Key key,
@required this.position,
@required this.allowImplicitScrolling,
@required this.semanticChildCount,
Widget child
}) : assert(position != null), super(key: key, child: child);
final ScrollPosition position;
final bool allowImplicitScrolling;
final int semanticChildCount;
@override
_RenderScrollSemantics createRenderObject(BuildContext context) {
return _RenderScrollSemantics(
position: position,
allowImplicitScrolling: allowImplicitScrolling,
semanticChildCount: semanticChildCount,
);
}
......@@ -559,7 +582,8 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
renderObject
..allowImplicitScrolling = allowImplicitScrolling
..position = position;
..position = position
..semanticChildCount = semanticChildCount;
}
}
......@@ -567,9 +591,11 @@ class _RenderScrollSemantics extends RenderProxyBox {
_RenderScrollSemantics({
@required ScrollPosition position,
@required bool allowImplicitScrolling,
@required int semanticChildCount,
RenderBox child,
}) : _position = position,
_allowImplicitScrolling = allowImplicitScrolling,
_semanticChildCount = semanticChildCount,
assert(position != null), super(child) {
position.addListener(markNeedsSemanticsUpdate);
}
......@@ -597,6 +623,15 @@ class _RenderScrollSemantics extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
int get semanticChildCount => _semanticChildCount;
int _semanticChildCount;
set semanticChildCount(int value) {
if (value == semanticChildCount)
return;
_semanticChildCount = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -606,7 +641,8 @@ class _RenderScrollSemantics extends RenderProxyBox {
..hasImplicitScrolling = allowImplicitScrolling
..scrollPosition = _position.pixels
..scrollExtentMax = _position.maxScrollExtent
..scrollExtentMin = _position.minScrollExtent;
..scrollExtentMin = _position.minScrollExtent
..scrollChildCount = semanticChildCount;
}
}
......@@ -624,15 +660,20 @@ class _RenderScrollSemantics extends RenderProxyBox {
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
int firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling))
excluded.add(child);
else
else {
if (!child.hasFlag(SemanticsFlag.isHidden))
firstVisibleIndex ??= child.indexInParent;
included.add(child);
}
}
config.scrollIndex = firstVisibleIndex;
node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
}
......
......@@ -16,6 +16,25 @@ export 'package:flutter/rendering.dart' show
SliverGridDelegateWithFixedCrossAxisCount,
SliverGridDelegateWithMaxCrossAxisExtent;
// Examples can assume:
// SliverGridDelegateWithMaxCrossAxisExtent _gridDelegate;
/// A callback which produces a semantic index given a widget and the local index.
///
/// Return a null value to prevent a widget from receiving an index.
///
/// A semantic index is used to tag child semantic nodes for accessibility
/// announcements in scroll view.
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
/// * [SliverChildBuilderDelegate], for an explanation of how this is used to
/// generate indexes.
typedef SemanticIndexCallback = int Function(Widget widget, int localIndex);
int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
/// A delegate that supplies children for slivers.
///
/// Many slivers lazily construct their box children to avoid creating more
......@@ -188,10 +207,96 @@ abstract class SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// ## Accessibility
///
/// The [CustomScrollView] requires that its semantic children are annotated
/// using [IndexedSemantics]. This is done by default in the delegate with
/// the `addSemanticIndexes` parameter set to true.
///
/// If multiple delegates are used in a single scroll view, then the indexes
/// will not be correct by default. The `semanticIndexOffset` can be used to
/// offset the semantic indexes of each delegate so that the indexes are
/// monotonically increasing. For example, if a scroll view contains two
/// delegates where the first has 10 children contributing semantics, then the
/// second delegate should offset its children by 10.
///
/// ## Sample code
///
/// This sample code shows how to use `semanticIndexOffset` to handle multiple
/// delegates in a single scroll view.
///
/// ```dart
/// CustomScrollView(
/// semanticChildCount: 4,
/// slivers: <Widget>[
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return Text('...');
/// },
/// childCount: 2,
/// ),
/// ),
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return Text('...');
/// },
/// childCount: 2,
/// semanticIndexOffset: 2,
/// ),
/// ),
/// ],
/// )
/// ```
///
/// In certain cases, only a subset of child widgets should be annotated
/// with a semantic index. For example, in [new ListView.separated()] the
/// separators do not have an index assocaited with them. This is done by
/// providing a `semanticIndexCallback` which returns null for separators
/// indexes and rounds the non-separator indexes down by half.
///
/// ## Sample code
///
/// This sample code shows how to use `semanticIndexCallback` to handle
/// annotating a subset of child nodes with a semantic index. There is
/// a [Spacer] widget at odd indexes which should not have a semantic
/// index.
///
/// ```dart
/// CustomScrollView(
/// semanticChildCount: 5,
/// slivers: <Widget>[
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// if (index.isEven) {
/// return Text('...');
/// }
/// return Spacer();
/// },
/// semanticIndexCallback: (Widget widget, int localIndex) {
/// if (localIndex.isEven) {
/// return localIndex ~/ 2;
/// }
/// return null;
/// },
/// childCount: 10,
/// ),
/// ),
/// ],
/// )
/// ```
///
/// See also:
///
/// * [SliverChildListDelegate], which is a delegate that has an explicit list
/// of children.
/// * [IndexedSemantics], for an example of manually annotating child nodes
/// with semantic indexes.
class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// builder callback.
......@@ -203,6 +308,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
this.childCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(builder != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
......@@ -250,6 +358,27 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by wrapping the correct child widgets in an
/// indexed child semantics widget.
///
/// Defaults to true.
final bool addSemanticIndexes;
/// An initial offset to add to the semantic indexes generated by this widget.
///
/// Defaults to zero.
final int semanticIndexOffset;
/// A [SemanticIndexCallback] which is used when [addSemanticIndexes] is true.
///
/// Defaults to providing an index for each widget.
final SemanticIndexCallback semanticIndexCallback;
@override
Widget build(BuildContext context, int index) {
assert(builder != null);
......@@ -260,6 +389,11 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
return null;
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
......@@ -297,6 +431,28 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// ## Accessibility
///
/// The [CustomScrollView] requires that its semantic children are annotated
/// using [IndexedSemantics]. This is done by default in the delegate with
/// the `addSemanticIndexes` parameter set to true.
///
/// If multiple delegates are used in a single scroll view, then the indexes
/// will not be correct by default. The `semanticIndexOffset` can be used to
/// offset the semantic indexes of each delegate so that the indexes are
/// monotonically increasing. For example, if a scroll view contains two
/// delegates where the first has 10 children contributing semantics, then the
/// second delegate should offset its children by 10.
///
/// In certain cases, only a subset of child widgets should be annotated
/// with a semantic index. For example, in [new ListView.separated()] the
/// separators do not have an index assocaited with them. This is done by
/// providing a `semanticIndexCallback` which returns null for separators
/// indexes and rounds the non-separator indexes down by half.
///
/// See [SliverChildBuilderDelegate] for sample code using
/// `semanticIndexOffset` and `semanticIndexCallback`.
///
/// See also:
///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
......@@ -311,6 +467,9 @@ class SliverChildListDelegate extends SliverChildDelegate {
this.children, {
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(children != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
......@@ -340,6 +499,27 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by wrapping the correct child widgets in an
/// indexed child semantics widget.
///
/// Defaults to true.
final bool addSemanticIndexes;
/// An initial offset to add to the semantic indexes generated by this widget.
///
/// Defaults to zero.
final int semanticIndexOffset;
/// A [SemanticIndexCallback] which is used when [addSemanticIndexes] is true.
///
/// Defaults to providing an index for each widget.
final SemanticIndexCallback semanticIndexCallback;
/// The widgets to display.
final List<Widget> children;
......@@ -352,6 +532,11 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(child != null);
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
......
......@@ -348,6 +348,8 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
......@@ -438,6 +440,8 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
......
......@@ -70,6 +70,7 @@ void tests({ @required bool impliedMode }) {
child: ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible
cacheExtent: 0.0,
children: generateList(const Placeholder(), impliedMode: impliedMode),
......@@ -117,6 +118,7 @@ void tests({ @required bool impliedMode }) {
child: ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: generateList(
Container(height: 12.3, child: const Placeholder()), // about 50 widgets visible
......@@ -166,6 +168,7 @@ void tests({ @required bool impliedMode }) {
child: GridView.count(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
cacheExtent: 0.0,
......@@ -222,6 +225,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -305,6 +309,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -360,6 +365,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -423,6 +429,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -468,6 +475,7 @@ void main() {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
addSemanticIndexes: false,
itemCount: 50,
itemBuilder: (BuildContext context, int index){
if (index == 0){
......@@ -501,6 +509,7 @@ void main() {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
addSemanticIndexes: false,
itemCount: 250,
itemBuilder: (BuildContext context, int index){
if (index % 2 == 0){
......
......@@ -1658,6 +1658,7 @@ void main() {
await tester.pumpWidget(MaterialApp(
home: ListView(
scrollDirection: Axis.horizontal,
addSemanticIndexes: false,
children: <Widget>[
DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
......
......@@ -48,6 +48,7 @@ Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWr
child: ListView(
scrollDirection: scrollDirection,
reverse: reverse,
addSemanticIndexes: false,
shrinkWrap: shrinkWrap,
children: <Widget>[
Container(key: const ValueKey<int>(0), width: 200.0, height: 200.0),
......
......@@ -50,6 +50,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()),
),
......@@ -97,6 +98,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
children: generateList(Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
),
),
......@@ -143,6 +145,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
children: generateList(Container(child: const Placeholder())),
......@@ -190,6 +193,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()),
),
......
......@@ -308,6 +308,7 @@ void main() {
textDirection: TextDirection.ltr,
child: ListView(
addAutomaticKeepAlives: false,
addSemanticIndexes: false,
children: <Widget>[
Container(height: 100.0),
Container(height: 100.0),
......
......@@ -42,6 +42,7 @@ void main() {
data: const MediaQueryData(),
child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 3000.0),
semanticChildCount: 30,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(listChildren),
......@@ -62,10 +63,15 @@ void main() {
TestSemantics(
children: <TestSemantics>[
TestSemantics(
scrollIndex: 15,
scrollChildren: 30,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
......@@ -77,6 +83,11 @@ void main() {
label: 'item 13b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 14a',
......@@ -87,6 +98,10 @@ void main() {
label: 'item 14b',
textDirection: TextDirection.ltr,
),
]
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Item 15a',
textDirection: TextDirection.ltr,
......@@ -95,6 +110,10 @@ void main() {
label: 'item 15b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Item 16a',
textDirection: TextDirection.ltr,
......@@ -103,6 +122,10 @@ void main() {
label: 'item 16b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Item 17a',
textDirection: TextDirection.ltr,
......@@ -111,6 +134,11 @@ void main() {
label: 'item 17b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 18a',
......@@ -121,6 +149,11 @@ void main() {
label: 'item 18b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 19a',
......@@ -139,6 +172,8 @@ void main() {
),
],
),
],
),
childOrder: DebugSemanticsDumpOrder.traversalOrder,
ignoreId: true,
ignoreTransform: true,
......@@ -181,7 +216,7 @@ void main() {
slivers: <Widget>[
SliverFixedExtentList(
itemExtent: 200.0,
delegate: SliverChildListDelegate(listChildren),
delegate: SliverChildListDelegate(listChildren, addSemanticIndexes: false),
),
],
),
......
......@@ -48,6 +48,8 @@ class TestSemantics {
this.transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
......@@ -73,6 +75,8 @@ class TestSemantics {
this.transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : id = 0,
assert(flags is int || flags is List<SemanticsFlag>),
......@@ -109,6 +113,8 @@ class TestSemantics {
Matrix4 transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
......@@ -200,6 +206,12 @@ class TestSemantics {
/// parent).
final Matrix4 transform;
/// The index of the first visible semantic node within a scrollable.
final int scrollIndex;
/// The total number of semantic nodes within a scrollable.
final int scrollChildren;
final TextSelection textSelection;
static Matrix4 _applyRootChildScale(Matrix4 transform) {
......@@ -270,6 +282,12 @@ class TestSemantics {
if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
return fail('expected node id $id to have textSelection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
}
if (scrollIndex != null && scrollIndex != nodeData.scrollIndex) {
return fail('expected node id $id to have scrollIndex $scrollIndex but found scrollIndex ${nodeData.scrollIndex}.');
}
if (scrollChildren != null && scrollChildren != nodeData.scrollChildCount) {
return fail('expected node id $id to have scrollIndex $scrollChildren but found scrollIndex ${nodeData.scrollChildCount}.');
}
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (children.length != childrenCount)
return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');
......@@ -322,6 +340,8 @@ class TestSemantics {
buf.writeln('$indent textDirection: $textDirection,');
if (textSelection?.isValid == true)
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (scrollIndex != null)
buf.writeln('$indent scrollIndex: $scrollIndex,');
if (rect != null)
buf.writeln('$indent rect: $rect,');
if (transform != null)
......
......@@ -17,7 +17,7 @@ void main() {
child: CustomScrollView(
slivers: <Widget>[
SliverFillViewport(
delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false),
delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false, addSemanticIndexes: false),
),
],
),
......
......@@ -495,6 +495,8 @@ void main() {
textDirection: TextDirection.ltr,
rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment