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>[ ...@@ -30,6 +30,8 @@ const List<String> coolColorNames = <String>[
'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre', 'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre',
]; ];
const int _kChildCount = 50;
class CupertinoNavigationDemo extends StatelessWidget { class CupertinoNavigationDemo extends StatelessWidget {
CupertinoNavigationDemo() CupertinoNavigationDemo()
: colorItems = List<Color>.generate(50, (int index) { : colorItems = List<Color>.generate(50, (int index) {
...@@ -146,6 +148,7 @@ class CupertinoDemoTab1 extends StatelessWidget { ...@@ -146,6 +148,7 @@ class CupertinoDemoTab1 extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CupertinoPageScaffold( return CupertinoPageScaffold(
child: CustomScrollView( child: CustomScrollView(
semanticChildCount: _kChildCount,
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverNavigationBar( CupertinoSliverNavigationBar(
trailing: trailingButtons, trailing: trailingButtons,
...@@ -163,12 +166,12 @@ class CupertinoDemoTab1 extends StatelessWidget { ...@@ -163,12 +166,12 @@ class CupertinoDemoTab1 extends StatelessWidget {
(BuildContext context, int index) { (BuildContext context, int index) {
return Tab1RowItem( return Tab1RowItem(
index: index, index: index,
lastItem: index == 49, lastItem: index == _kChildCount - 1,
color: colorItems[index], color: colorItems[index],
colorName: colorNameItems[index], colorName: colorNameItems[index],
); );
}, },
childCount: 50, childCount: _kChildCount,
), ),
), ),
), ),
......
...@@ -91,6 +91,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> { ...@@ -91,6 +91,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
}, },
), ),
body: CustomScrollView( body: CustomScrollView(
semanticChildCount: widget.recipes.length,
slivers: <Widget>[ slivers: <Widget>[
_buildAppBar(context, statusBarHeight), _buildAppBar(context, statusBarHeight),
_buildBody(context, statusBarHeight), _buildBody(context, statusBarHeight),
......
...@@ -4330,6 +4330,49 @@ class RenderExcludeSemantics extends RenderProxyBox { ...@@ -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]. /// Provides an anchor for a [RenderFollowerLayer].
/// ///
/// See also: /// See also:
......
...@@ -188,6 +188,8 @@ class SemanticsData extends Diagnosticable { ...@@ -188,6 +188,8 @@ class SemanticsData extends Diagnosticable {
@required this.textDirection, @required this.textDirection,
@required this.rect, @required this.rect,
@required this.textSelection, @required this.textSelection,
@required this.scrollIndex,
@required this.scrollChildCount,
@required this.scrollPosition, @required this.scrollPosition,
@required this.scrollExtentMax, @required this.scrollExtentMax,
@required this.scrollExtentMin, @required this.scrollExtentMin,
...@@ -249,6 +251,15 @@ class SemanticsData extends Diagnosticable { ...@@ -249,6 +251,15 @@ class SemanticsData extends Diagnosticable {
/// if this node represents a text field. /// if this node represents a text field.
final TextSelection textSelection; 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 /// Indicates the current scrolling position in logical pixels if the node is
/// scrollable. /// scrollable.
/// ///
...@@ -343,6 +354,8 @@ class SemanticsData extends Diagnosticable { ...@@ -343,6 +354,8 @@ class SemanticsData extends Diagnosticable {
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid == true) if (textSelection?.isValid == true)
properties.add(MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]')); 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('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null)); properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null)); properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
...@@ -363,6 +376,8 @@ class SemanticsData extends Diagnosticable { ...@@ -363,6 +376,8 @@ class SemanticsData extends Diagnosticable {
&& typedOther.textDirection == textDirection && typedOther.textDirection == textDirection
&& typedOther.rect == rect && typedOther.rect == rect
&& setEquals(typedOther.tags, tags) && setEquals(typedOther.tags, tags)
&& typedOther.scrollChildCount == scrollChildCount
&& typedOther.scrollIndex == scrollIndex
&& typedOther.textSelection == textSelection && typedOther.textSelection == textSelection
&& typedOther.scrollPosition == scrollPosition && typedOther.scrollPosition == scrollPosition
&& typedOther.scrollExtentMax == scrollExtentMax && typedOther.scrollExtentMax == scrollExtentMax
...@@ -385,6 +400,8 @@ class SemanticsData extends Diagnosticable { ...@@ -385,6 +400,8 @@ class SemanticsData extends Diagnosticable {
rect, rect,
tags, tags,
textSelection, textSelection,
scrollChildCount,
scrollIndex,
scrollPosition, scrollPosition,
scrollExtentMax, scrollExtentMax,
scrollExtentMin, scrollExtentMin,
...@@ -1125,6 +1142,14 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1125,6 +1142,14 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// If this rect is null [parentSemanticsClipRect] also has to be null. /// If this rect is null [parentSemanticsClipRect] also has to be null.
Rect parentPaintClipRect; 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. /// Whether the node is invisible.
/// ///
/// A node whose [rect] is outside of the bounds of the screen and hence not /// 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 { ...@@ -1394,6 +1419,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollExtentMax != config._scrollExtentMax || _scrollExtentMax != config._scrollExtentMax ||
_scrollExtentMin != config._scrollExtentMin || _scrollExtentMin != config._scrollExtentMin ||
_actionsAsBits != config._actionsAsBits || _actionsAsBits != config._actionsAsBits ||
indexInParent != config.indexInParent ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants; _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
} }
...@@ -1415,7 +1441,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1415,7 +1441,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
int _flags = _kEmptyConfig._flags; 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. /// A textual description of this node.
/// ///
...@@ -1479,6 +1506,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1479,6 +1506,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextSelection get textSelection => _textSelection; TextSelection get textSelection => _textSelection;
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 /// Indicates the current scrolling position in logical pixels if the node is
/// scrollable. /// scrollable.
/// ///
...@@ -1553,6 +1591,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1553,6 +1591,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_scrollExtentMax = config._scrollExtentMax; _scrollExtentMax = config._scrollExtentMax;
_scrollExtentMin = config._scrollExtentMin; _scrollExtentMin = config._scrollExtentMin;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants; _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_scrollChildCount = config.scrollChildCount;
_scrollIndex = config.scrollIndex;
indexInParent = config.indexInParent;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]); _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert( assert(
...@@ -1582,6 +1623,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1582,6 +1623,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextDirection textDirection = _textDirection; TextDirection textDirection = _textDirection;
Set<SemanticsTag> mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags); Set<SemanticsTag> mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags);
TextSelection textSelection = _textSelection; TextSelection textSelection = _textSelection;
int scrollChildCount = _scrollChildCount;
int scrollIndex = _scrollIndex;
double scrollPosition = _scrollPosition; double scrollPosition = _scrollPosition;
double scrollExtentMax = _scrollExtentMax; double scrollExtentMax = _scrollExtentMax;
double scrollExtentMin = _scrollExtentMin; double scrollExtentMin = _scrollExtentMin;
...@@ -1612,6 +1655,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1612,6 +1655,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
actions |= node._actionsAsBits; actions |= node._actionsAsBits;
textDirection ??= node._textDirection; textDirection ??= node._textDirection;
textSelection ??= node._textSelection; textSelection ??= node._textSelection;
scrollChildCount ??= node._scrollChildCount;
scrollIndex ??= node._scrollIndex;
scrollPosition ??= node._scrollPosition; scrollPosition ??= node._scrollPosition;
scrollExtentMax ??= node._scrollExtentMax; scrollExtentMax ??= node._scrollExtentMax;
scrollExtentMin ??= node._scrollExtentMin; scrollExtentMin ??= node._scrollExtentMin;
...@@ -1674,6 +1719,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1674,6 +1719,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
transform: transform, transform: transform,
tags: mergedTags, tags: mergedTags,
textSelection: textSelection, textSelection: textSelection,
scrollChildCount: scrollChildCount,
scrollIndex: scrollIndex,
scrollPosition: scrollPosition, scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax, scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin, scrollExtentMin: scrollExtentMin,
...@@ -1732,6 +1779,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1732,6 +1779,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
textDirection: data.textDirection, textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1, textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -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, scrollPosition: data.scrollPosition != null ? data.scrollPosition : double.nan,
scrollExtentMax: data.scrollExtentMax != null ? data.scrollExtentMax : double.nan, scrollExtentMax: data.scrollExtentMax != null ? data.scrollExtentMax : double.nan,
scrollExtentMin: data.scrollExtentMin != null ? data.scrollExtentMin : double.nan, scrollExtentMin: data.scrollExtentMin != null ? data.scrollExtentMin : double.nan,
...@@ -1855,10 +1904,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1855,10 +1904,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
.toList(); .toList();
properties.add(IterableProperty<String>('actions', actions, ifEmpty: null)); properties.add(IterableProperty<String>('actions', actions, ifEmpty: null));
properties.add(IterableProperty<String>('customActions', customSemanticsActions, 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(IterableProperty<String>('flags', flags, ifEmpty: null));
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible')); 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('label', _label, defaultValue: ''));
properties.add(StringProperty('value', _value, defaultValue: '')); properties.add(StringProperty('value', _value, defaultValue: ''));
properties.add(StringProperty('increasedValue', _increasedValue, defaultValue: '')); properties.add(StringProperty('increasedValue', _increasedValue, defaultValue: ''));
...@@ -1868,6 +1917,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1868,6 +1917,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid == true) if (_textSelection?.isValid == true)
properties.add(MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]')); 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('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null)); properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null)); properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
...@@ -2889,6 +2940,44 @@ class SemanticsConfiguration { ...@@ -2889,6 +2940,44 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true; _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 /// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity. /// all of its descendants should be treated as one logical entity.
/// ///
...@@ -3358,6 +3447,9 @@ class SemanticsConfiguration { ...@@ -3358,6 +3447,9 @@ class SemanticsConfiguration {
_scrollExtentMax ??= other._scrollExtentMax; _scrollExtentMax ??= other._scrollExtentMax;
_scrollExtentMin ??= other._scrollExtentMin; _scrollExtentMin ??= other._scrollExtentMin;
_hintOverrides ??= other._hintOverrides; _hintOverrides ??= other._hintOverrides;
_indexInParent ??= other.indexInParent;
_scrollIndex ??= other._scrollIndex;
_scrollChildCount ??= other._scrollChildCount;
textDirection ??= other.textDirection; textDirection ??= other.textDirection;
_sortKey ??= other._sortKey; _sortKey ??= other._sortKey;
...@@ -3406,6 +3498,9 @@ class SemanticsConfiguration { ...@@ -3406,6 +3498,9 @@ class SemanticsConfiguration {
.._scrollExtentMax = _scrollExtentMax .._scrollExtentMax = _scrollExtentMax
.._scrollExtentMin = _scrollExtentMin .._scrollExtentMin = _scrollExtentMin
.._actionsAsBits = _actionsAsBits .._actionsAsBits = _actionsAsBits
.._indexInParent = indexInParent
.._scrollIndex = _scrollIndex
.._scrollChildCount = _scrollChildCount
.._actions.addAll(_actions) .._actions.addAll(_actions)
.._customSemanticsActions.addAll(_customSemanticsActions); .._customSemanticsActions.addAll(_customSemanticsActions);
} }
......
...@@ -5553,6 +5553,64 @@ class ExcludeSemantics extends SingleChildRenderObjectWidget { ...@@ -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. /// A widget that builds its child.
/// ///
/// Useful for attaching a key to an existing widget. /// Useful for attaching a key to an existing widget.
......
...@@ -60,6 +60,7 @@ abstract class ScrollView extends StatelessWidget { ...@@ -60,6 +60,7 @@ abstract class ScrollView extends StatelessWidget {
ScrollPhysics physics, ScrollPhysics physics,
this.shrinkWrap = false, this.shrinkWrap = false,
this.cacheExtent, this.cacheExtent,
this.semanticChildCount,
}) : assert(reverse != null), }) : assert(reverse != null),
assert(shrinkWrap != null), assert(shrinkWrap != null),
assert(!(controller != null && primary == true), assert(!(controller != null && primary == true),
...@@ -172,6 +173,21 @@ abstract class ScrollView extends StatelessWidget { ...@@ -172,6 +173,21 @@ abstract class ScrollView extends StatelessWidget {
/// {@macro flutter.rendering.viewport.cacheExtent} /// {@macro flutter.rendering.viewport.cacheExtent}
final double 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. /// Returns the [AxisDirection] in which the scroll view scrolls.
/// ///
/// Combines the [scrollDirection] with the [reverse] boolean to obtain the /// Combines the [scrollDirection] with the [reverse] boolean to obtain the
...@@ -234,6 +250,7 @@ abstract class ScrollView extends StatelessWidget { ...@@ -234,6 +250,7 @@ abstract class ScrollView extends StatelessWidget {
axisDirection: axisDirection, axisDirection: axisDirection,
controller: scrollController, controller: scrollController,
physics: physics, physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers); return buildViewport(context, offset, axisDirection, slivers);
}, },
...@@ -317,6 +334,40 @@ abstract class ScrollView extends StatelessWidget { ...@@ -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: /// See also:
/// ///
/// * [SliverList], which is a sliver that displays linear list of children. /// * [SliverList], which is a sliver that displays linear list of children.
...@@ -329,6 +380,8 @@ abstract class ScrollView extends StatelessWidget { ...@@ -329,6 +380,8 @@ abstract class ScrollView extends StatelessWidget {
/// and float as the scroll view scrolls. /// and float as the scroll view scrolls.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch /// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController]. /// the scroll position without using a [ScrollController].
/// * [IndexedSemantics], which allows annotating child lists with an index
/// for scroll announcements.
class CustomScrollView extends ScrollView { class CustomScrollView extends ScrollView {
/// Creates a [ScrollView] that creates custom scroll effects using slivers. /// Creates a [ScrollView] that creates custom scroll effects using slivers.
/// ///
...@@ -343,6 +396,7 @@ class CustomScrollView extends ScrollView { ...@@ -343,6 +396,7 @@ class CustomScrollView extends ScrollView {
bool shrinkWrap = false, bool shrinkWrap = false,
double cacheExtent, double cacheExtent,
this.slivers = const <Widget>[], this.slivers = const <Widget>[],
int semanticChildCount,
}) : super( }) : super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -352,6 +406,7 @@ class CustomScrollView extends ScrollView { ...@@ -352,6 +406,7 @@ class CustomScrollView extends ScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
); );
/// The slivers to place inside the viewport. /// The slivers to place inside the viewport.
...@@ -383,6 +438,7 @@ abstract class BoxScrollView extends ScrollView { ...@@ -383,6 +438,7 @@ abstract class BoxScrollView extends ScrollView {
bool shrinkWrap = false, bool shrinkWrap = false,
this.padding, this.padding,
double cacheExtent, double cacheExtent,
int semanticChildCount,
}) : super( }) : super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -392,6 +448,7 @@ abstract class BoxScrollView extends ScrollView { ...@@ -392,6 +448,7 @@ abstract class BoxScrollView extends ScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
); );
/// The amount of space by which to inset the children. /// The amount of space by which to inset the children.
...@@ -672,12 +729,15 @@ class ListView extends BoxScrollView { ...@@ -672,12 +729,15 @@ class ListView extends BoxScrollView {
this.itemExtent, this.itemExtent,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
List<Widget> children = const <Widget>[], List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : childrenDelegate = SliverChildListDelegate( }) : childrenDelegate = SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super( ), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -688,6 +748,7 @@ class ListView extends BoxScrollView { ...@@ -688,6 +748,7 @@ class ListView extends BoxScrollView {
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
); );
/// Creates a scrollable, linear array of widgets that are created on demand. /// Creates a scrollable, linear array of widgets that are created on demand.
...@@ -728,12 +789,15 @@ class ListView extends BoxScrollView { ...@@ -728,12 +789,15 @@ class ListView extends BoxScrollView {
int itemCount, int itemCount,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
int semanticChildCount,
}) : childrenDelegate = SliverChildBuilderDelegate( }) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
childCount: itemCount, childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super( ), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -743,7 +807,8 @@ class ListView extends BoxScrollView { ...@@ -743,7 +807,8 @@ class ListView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
); );
/// Creates a fixed-length scrollable linear array of list "items" separated /// Creates a fixed-length scrollable linear array of list "items" separated
...@@ -804,6 +869,7 @@ class ListView extends BoxScrollView { ...@@ -804,6 +869,7 @@ class ListView extends BoxScrollView {
@required int itemCount, @required int itemCount,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(separatorBuilder != null), assert(separatorBuilder != null),
...@@ -816,9 +882,13 @@ class ListView extends BoxScrollView { ...@@ -816,9 +882,13 @@ class ListView extends BoxScrollView {
? itemBuilder(context, itemIndex) ? itemBuilder(context, itemIndex)
: separatorBuilder(context, itemIndex); : separatorBuilder(context, itemIndex);
}, },
childCount: math.max(0, itemCount * 2 - 1), childCount: _computeSemanticChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
semanticIndexCallback: (Widget _, int index) {
return index.isEven ? index ~/ 2 : null;
}
), super( ), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -828,7 +898,8 @@ class ListView extends BoxScrollView { ...@@ -828,7 +898,8 @@ class ListView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent cacheExtent: cacheExtent,
semanticChildCount: _computeSemanticChildCount(itemCount),
); );
/// Creates a scrollable, linear array of widgets with a custom child model. /// Creates a scrollable, linear array of widgets with a custom child model.
...@@ -847,6 +918,7 @@ class ListView extends BoxScrollView { ...@@ -847,6 +918,7 @@ class ListView extends BoxScrollView {
this.itemExtent, this.itemExtent,
@required this.childrenDelegate, @required this.childrenDelegate,
double cacheExtent, double cacheExtent,
int semanticChildCount,
}) : assert(childrenDelegate != null), }) : assert(childrenDelegate != null),
super( super(
key: key, key: key,
...@@ -858,6 +930,7 @@ class ListView extends BoxScrollView { ...@@ -858,6 +930,7 @@ class ListView extends BoxScrollView {
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
); );
/// If non-null, forces the children to have the given extent in the scroll /// If non-null, forces the children to have the given extent in the scroll
...@@ -893,6 +966,11 @@ class ListView extends BoxScrollView { ...@@ -893,6 +966,11 @@ class ListView extends BoxScrollView {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DoubleProperty('itemExtent', itemExtent, defaultValue: null)); 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. /// A scrollable, 2D array of widgets.
...@@ -1047,13 +1125,16 @@ class GridView extends BoxScrollView { ...@@ -1047,13 +1125,16 @@ class GridView extends BoxScrollView {
@required this.gridDelegate, @required this.gridDelegate,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
List<Widget> children = const <Widget>[], List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = SliverChildListDelegate( childrenDelegate = SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), ),
super( super(
key: key, key: key,
...@@ -1064,7 +1145,8 @@ class GridView extends BoxScrollView { ...@@ -1064,7 +1145,8 @@ class GridView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
); );
/// Creates a scrollable, 2D array of widgets that are created on demand. /// Creates a scrollable, 2D array of widgets that are created on demand.
...@@ -1100,13 +1182,16 @@ class GridView extends BoxScrollView { ...@@ -1100,13 +1182,16 @@ class GridView extends BoxScrollView {
int itemCount, int itemCount,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
int semanticChildCount,
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = SliverChildBuilderDelegate( childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
childCount: itemCount, childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), ),
super( super(
key: key, key: key,
...@@ -1117,7 +1202,8 @@ class GridView extends BoxScrollView { ...@@ -1117,7 +1202,8 @@ class GridView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
); );
/// Creates a scrollable, 2D array of widgets with both a custom /// Creates a scrollable, 2D array of widgets with both a custom
...@@ -1139,6 +1225,7 @@ class GridView extends BoxScrollView { ...@@ -1139,6 +1225,7 @@ class GridView extends BoxScrollView {
@required this.gridDelegate, @required this.gridDelegate,
@required this.childrenDelegate, @required this.childrenDelegate,
double cacheExtent, double cacheExtent,
int semanticChildCount,
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
assert(childrenDelegate != null), assert(childrenDelegate != null),
super( super(
...@@ -1150,7 +1237,8 @@ class GridView extends BoxScrollView { ...@@ -1150,7 +1237,8 @@ class GridView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
); );
/// Creates a scrollable, 2D array of widgets with a fixed number of tiles in /// Creates a scrollable, 2D array of widgets with a fixed number of tiles in
...@@ -1182,8 +1270,10 @@ class GridView extends BoxScrollView { ...@@ -1182,8 +1270,10 @@ class GridView extends BoxScrollView {
double childAspectRatio = 1.0, double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent, double cacheExtent,
List<Widget> children = const <Widget>[], List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing, mainAxisSpacing: mainAxisSpacing,
...@@ -1194,6 +1284,7 @@ class GridView extends BoxScrollView { ...@@ -1194,6 +1284,7 @@ class GridView extends BoxScrollView {
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super( ), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -1204,6 +1295,7 @@ class GridView extends BoxScrollView { ...@@ -1204,6 +1295,7 @@ class GridView extends BoxScrollView {
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
); );
/// Creates a scrollable, 2D array of widgets with tiles that each have a /// Creates a scrollable, 2D array of widgets with tiles that each have a
...@@ -1235,7 +1327,9 @@ class GridView extends BoxScrollView { ...@@ -1235,7 +1327,9 @@ class GridView extends BoxScrollView {
double childAspectRatio = 1.0, double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
List<Widget> children = const <Widget>[], List<Widget> children = const <Widget>[],
int semanticChildCount,
}) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent, maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing, mainAxisSpacing: mainAxisSpacing,
...@@ -1246,6 +1340,7 @@ class GridView extends BoxScrollView { ...@@ -1246,6 +1340,7 @@ class GridView extends BoxScrollView {
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super( ), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -1255,6 +1350,7 @@ class GridView extends BoxScrollView { ...@@ -1255,6 +1350,7 @@ class GridView extends BoxScrollView {
physics: physics, physics: physics,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
padding: padding, padding: padding,
semanticChildCount: semanticChildCount ?? children.length,
); );
/// A delegate that controls the layout of the children within the [GridView]. /// A delegate that controls the layout of the children within the [GridView].
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -79,6 +80,7 @@ class Scrollable extends StatefulWidget { ...@@ -79,6 +80,7 @@ class Scrollable extends StatefulWidget {
this.physics, this.physics,
@required this.viewportBuilder, @required this.viewportBuilder,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.semanticChildCount,
}) : assert(axisDirection != null), }) : assert(axisDirection != null),
assert(viewportBuilder != null), assert(viewportBuilder != null),
assert(excludeFromSemantics != null), assert(excludeFromSemantics != null),
...@@ -161,6 +163,23 @@ class Scrollable extends StatefulWidget { ...@@ -161,6 +163,23 @@ class Scrollable extends StatefulWidget {
/// exclusion. /// exclusion.
final bool excludeFromSemantics; 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. /// The axis along which the scroll view scrolls.
/// ///
/// Determined by the [axisDirection]. /// Determined by the [axisDirection].
...@@ -509,6 +528,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -509,6 +528,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
child: result, child: result,
position: position, position: position,
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? false, allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? false,
semanticChildCount: widget.semanticChildCount,
); );
} }
...@@ -541,17 +561,20 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { ...@@ -541,17 +561,20 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
Key key, Key key,
@required this.position, @required this.position,
@required this.allowImplicitScrolling, @required this.allowImplicitScrolling,
@required this.semanticChildCount,
Widget child Widget child
}) : assert(position != null), super(key: key, child: child); }) : assert(position != null), super(key: key, child: child);
final ScrollPosition position; final ScrollPosition position;
final bool allowImplicitScrolling; final bool allowImplicitScrolling;
final int semanticChildCount;
@override @override
_RenderScrollSemantics createRenderObject(BuildContext context) { _RenderScrollSemantics createRenderObject(BuildContext context) {
return _RenderScrollSemantics( return _RenderScrollSemantics(
position: position, position: position,
allowImplicitScrolling: allowImplicitScrolling, allowImplicitScrolling: allowImplicitScrolling,
semanticChildCount: semanticChildCount,
); );
} }
...@@ -559,7 +582,8 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget { ...@@ -559,7 +582,8 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) { void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
renderObject renderObject
..allowImplicitScrolling = allowImplicitScrolling ..allowImplicitScrolling = allowImplicitScrolling
..position = position; ..position = position
..semanticChildCount = semanticChildCount;
} }
} }
...@@ -567,9 +591,11 @@ class _RenderScrollSemantics extends RenderProxyBox { ...@@ -567,9 +591,11 @@ class _RenderScrollSemantics extends RenderProxyBox {
_RenderScrollSemantics({ _RenderScrollSemantics({
@required ScrollPosition position, @required ScrollPosition position,
@required bool allowImplicitScrolling, @required bool allowImplicitScrolling,
@required int semanticChildCount,
RenderBox child, RenderBox child,
}) : _position = position, }) : _position = position,
_allowImplicitScrolling = allowImplicitScrolling, _allowImplicitScrolling = allowImplicitScrolling,
_semanticChildCount = semanticChildCount,
assert(position != null), super(child) { assert(position != null), super(child) {
position.addListener(markNeedsSemanticsUpdate); position.addListener(markNeedsSemanticsUpdate);
} }
...@@ -597,6 +623,15 @@ class _RenderScrollSemantics extends RenderProxyBox { ...@@ -597,6 +623,15 @@ class _RenderScrollSemantics extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
int get semanticChildCount => _semanticChildCount;
int _semanticChildCount;
set semanticChildCount(int value) {
if (value == semanticChildCount)
return;
_semanticChildCount = value;
markNeedsSemanticsUpdate();
}
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
...@@ -606,7 +641,8 @@ class _RenderScrollSemantics extends RenderProxyBox { ...@@ -606,7 +641,8 @@ class _RenderScrollSemantics extends RenderProxyBox {
..hasImplicitScrolling = allowImplicitScrolling ..hasImplicitScrolling = allowImplicitScrolling
..scrollPosition = _position.pixels ..scrollPosition = _position.pixels
..scrollExtentMax = _position.maxScrollExtent ..scrollExtentMax = _position.maxScrollExtent
..scrollExtentMin = _position.minScrollExtent; ..scrollExtentMin = _position.minScrollExtent
..scrollChildCount = semanticChildCount;
} }
} }
...@@ -624,15 +660,20 @@ class _RenderScrollSemantics extends RenderProxyBox { ...@@ -624,15 +660,20 @@ class _RenderScrollSemantics extends RenderProxyBox {
..isMergedIntoParent = node.isPartOfNodeMerging ..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size; ..rect = Offset.zero & node.rect.size;
int firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode]; final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[]; final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) { for (SemanticsNode child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling)) if (child.isTagged(RenderViewport.excludeFromScrolling))
excluded.add(child); excluded.add(child);
else else {
if (!child.hasFlag(SemanticsFlag.isHidden))
firstVisibleIndex ??= child.indexInParent;
included.add(child); included.add(child);
}
} }
config.scrollIndex = firstVisibleIndex;
node.updateWith(config: null, childrenInInversePaintOrder: excluded); node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included); _innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
} }
......
...@@ -16,6 +16,25 @@ export 'package:flutter/rendering.dart' show ...@@ -16,6 +16,25 @@ export 'package:flutter/rendering.dart' show
SliverGridDelegateWithFixedCrossAxisCount, SliverGridDelegateWithFixedCrossAxisCount,
SliverGridDelegateWithMaxCrossAxisExtent; 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. /// A delegate that supplies children for slivers.
/// ///
/// Many slivers lazily construct their box children to avoid creating more /// Many slivers lazily construct their box children to avoid creating more
...@@ -188,10 +207,96 @@ abstract class SliverChildDelegate { ...@@ -188,10 +207,96 @@ abstract class SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true /// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default). /// (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: /// See also:
/// ///
/// * [SliverChildListDelegate], which is a delegate that has an explicit list /// * [SliverChildListDelegate], which is a delegate that has an explicit list
/// of children. /// of children.
/// * [IndexedSemantics], for an example of manually annotating child nodes
/// with semantic indexes.
class SliverChildBuilderDelegate extends SliverChildDelegate { class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given /// Creates a delegate that supplies children for slivers using the given
/// builder callback. /// builder callback.
...@@ -203,6 +308,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -203,6 +308,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
this.childCount, this.childCount,
this.addAutomaticKeepAlives = true, this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true, this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(builder != null), }) : assert(builder != null),
assert(addAutomaticKeepAlives != null), assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null); assert(addRepaintBoundaries != null);
...@@ -250,6 +358,27 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -250,6 +358,27 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Defaults to true. /// Defaults to true.
final bool addRepaintBoundaries; 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 @override
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
assert(builder != null); assert(builder != null);
...@@ -260,6 +389,11 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -260,6 +389,11 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
return null; return null;
if (addRepaintBoundaries) if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index); 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) if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child); child = AutomaticKeepAlive(child: child);
return child; return child;
...@@ -297,6 +431,28 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -297,6 +431,28 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true /// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default). /// (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: /// See also:
/// ///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder /// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
...@@ -311,6 +467,9 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -311,6 +467,9 @@ class SliverChildListDelegate extends SliverChildDelegate {
this.children, { this.children, {
this.addAutomaticKeepAlives = true, this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true, this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(children != null), }) : assert(children != null),
assert(addAutomaticKeepAlives != null), assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null); assert(addRepaintBoundaries != null);
...@@ -340,6 +499,27 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -340,6 +499,27 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// Defaults to true. /// Defaults to true.
final bool addRepaintBoundaries; 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. /// The widgets to display.
final List<Widget> children; final List<Widget> children;
...@@ -352,6 +532,11 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -352,6 +532,11 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(child != null); assert(child != null);
if (addRepaintBoundaries) if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index); 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) if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child); child = AutomaticKeepAlive(child: child);
return child; return child;
......
...@@ -348,6 +348,8 @@ void main() { ...@@ -348,6 +348,8 @@ void main() {
' hint: ""\n' ' hint: ""\n'
' textDirection: null\n' ' textDirection: null\n'
' sortKey: null\n' ' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n' ' scrollExtentMin: null\n'
' scrollPosition: null\n' ' scrollPosition: null\n'
' scrollExtentMax: null\n' ' scrollExtentMax: null\n'
...@@ -438,6 +440,8 @@ void main() { ...@@ -438,6 +440,8 @@ void main() {
' hint: ""\n' ' hint: ""\n'
' textDirection: null\n' ' textDirection: null\n'
' sortKey: null\n' ' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n' ' scrollExtentMin: null\n'
' scrollPosition: null\n' ' scrollPosition: null\n'
' scrollExtentMax: null\n' ' scrollExtentMax: null\n'
......
...@@ -70,6 +70,7 @@ void tests({ @required bool impliedMode }) { ...@@ -70,6 +70,7 @@ void tests({ @required bool impliedMode }) {
child: ListView( child: ListView(
addAutomaticKeepAlives: impliedMode, addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode, addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible itemExtent: 12.3, // about 50 widgets visible
cacheExtent: 0.0, cacheExtent: 0.0,
children: generateList(const Placeholder(), impliedMode: impliedMode), children: generateList(const Placeholder(), impliedMode: impliedMode),
...@@ -117,6 +118,7 @@ void tests({ @required bool impliedMode }) { ...@@ -117,6 +118,7 @@ void tests({ @required bool impliedMode }) {
child: ListView( child: ListView(
addAutomaticKeepAlives: impliedMode, addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode, addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
cacheExtent: 0.0, cacheExtent: 0.0,
children: generateList( children: generateList(
Container(height: 12.3, child: const Placeholder()), // about 50 widgets visible Container(height: 12.3, child: const Placeholder()), // about 50 widgets visible
...@@ -166,6 +168,7 @@ void tests({ @required bool impliedMode }) { ...@@ -166,6 +168,7 @@ void tests({ @required bool impliedMode }) {
child: GridView.count( child: GridView.count(
addAutomaticKeepAlives: impliedMode, addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode, addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
crossAxisCount: 2, crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
cacheExtent: 0.0, cacheExtent: 0.0,
...@@ -222,6 +225,7 @@ void main() { ...@@ -222,6 +225,7 @@ void main() {
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0, cacheExtent: 0.0,
children: <Widget>[ children: <Widget>[
AutomaticKeepAlive( AutomaticKeepAlive(
...@@ -305,6 +309,7 @@ void main() { ...@@ -305,6 +309,7 @@ void main() {
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0, cacheExtent: 0.0,
children: <Widget>[ children: <Widget>[
AutomaticKeepAlive( AutomaticKeepAlive(
...@@ -360,6 +365,7 @@ void main() { ...@@ -360,6 +365,7 @@ void main() {
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0, cacheExtent: 0.0,
children: <Widget>[ children: <Widget>[
AutomaticKeepAlive( AutomaticKeepAlive(
...@@ -423,6 +429,7 @@ void main() { ...@@ -423,6 +429,7 @@ void main() {
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0, cacheExtent: 0.0,
children: <Widget>[ children: <Widget>[
AutomaticKeepAlive( AutomaticKeepAlive(
...@@ -468,6 +475,7 @@ void main() { ...@@ -468,6 +475,7 @@ void main() {
await tester.pumpWidget(Directionality( await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ListView.builder( child: ListView.builder(
addSemanticIndexes: false,
itemCount: 50, itemCount: 50,
itemBuilder: (BuildContext context, int index){ itemBuilder: (BuildContext context, int index){
if (index == 0){ if (index == 0){
...@@ -501,6 +509,7 @@ void main() { ...@@ -501,6 +509,7 @@ void main() {
await tester.pumpWidget(Directionality( await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ListView.builder( child: ListView.builder(
addSemanticIndexes: false,
itemCount: 250, itemCount: 250,
itemBuilder: (BuildContext context, int index){ itemBuilder: (BuildContext context, int index){
if (index % 2 == 0){ if (index % 2 == 0){
......
...@@ -1658,6 +1658,7 @@ void main() { ...@@ -1658,6 +1658,7 @@ void main() {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: ListView( home: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
addSemanticIndexes: false,
children: <Widget>[ children: <Widget>[
DragTarget<int>( DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) { builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
......
...@@ -48,6 +48,7 @@ Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWr ...@@ -48,6 +48,7 @@ Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWr
child: ListView( child: ListView(
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
addSemanticIndexes: false,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
children: <Widget>[ children: <Widget>[
Container(key: const ValueKey<int>(0), width: 200.0, height: 200.0), Container(key: const ValueKey<int>(0), width: 200.0, height: 200.0),
......
...@@ -50,6 +50,7 @@ void main() { ...@@ -50,6 +50,7 @@ void main() {
cacheExtent: 0.0, cacheExtent: 0.0,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()), children: generateList(const Placeholder()),
), ),
...@@ -97,6 +98,7 @@ void main() { ...@@ -97,6 +98,7 @@ void main() {
cacheExtent: 0.0, cacheExtent: 0.0,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
children: generateList(Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible children: generateList(Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
), ),
), ),
...@@ -143,6 +145,7 @@ void main() { ...@@ -143,6 +145,7 @@ void main() {
cacheExtent: 0.0, cacheExtent: 0.0,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
crossAxisCount: 2, crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
children: generateList(Container(child: const Placeholder())), children: generateList(Container(child: const Placeholder())),
...@@ -190,6 +193,7 @@ void main() { ...@@ -190,6 +193,7 @@ void main() {
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 400.0, // 2 visible children itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()), children: generateList(const Placeholder()),
), ),
......
...@@ -308,6 +308,7 @@ void main() { ...@@ -308,6 +308,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ListView( child: ListView(
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
addSemanticIndexes: false,
children: <Widget>[ children: <Widget>[
Container(height: 100.0), Container(height: 100.0),
Container(height: 100.0), Container(height: 100.0),
......
...@@ -42,6 +42,7 @@ void main() { ...@@ -42,6 +42,7 @@ void main() {
data: const MediaQueryData(), data: const MediaQueryData(),
child: CustomScrollView( child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 3000.0), controller: ScrollController(initialScrollOffset: 3000.0),
semanticChildCount: 30,
slivers: <Widget>[ slivers: <Widget>[
SliverList( SliverList(
delegate: SliverChildListDelegate(listChildren), delegate: SliverChildListDelegate(listChildren),
...@@ -62,6 +63,8 @@ void main() { ...@@ -62,6 +63,8 @@ void main() {
TestSemantics( TestSemantics(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
scrollIndex: 15,
scrollChildren: 30,
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.scrollUp, SemanticsAction.scrollUp,
SemanticsAction.scrollDown, SemanticsAction.scrollDown,
...@@ -69,67 +72,99 @@ void main() { ...@@ -69,67 +72,99 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 13a', children: <TestSemantics>[
textDirection: TextDirection.ltr, TestSemantics(
), flags: <SemanticsFlag>[SemanticsFlag.isHidden],
TestSemantics( label: 'Item 13a',
flags: <SemanticsFlag>[SemanticsFlag.isHidden], textDirection: TextDirection.ltr,
label: 'item 13b', ),
textDirection: TextDirection.ltr, TestSemantics(
), flags: <SemanticsFlag>[SemanticsFlag.isHidden],
TestSemantics( label: 'item 13b',
flags: <SemanticsFlag>[SemanticsFlag.isHidden], textDirection: TextDirection.ltr,
label: 'Item 14a', ),
textDirection: TextDirection.ltr, ],
), ),
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 14b', children: <TestSemantics>[
textDirection: TextDirection.ltr, TestSemantics(
), flags: <SemanticsFlag>[SemanticsFlag.isHidden],
TestSemantics( label: 'Item 14a',
label: 'Item 15a', textDirection: TextDirection.ltr,
textDirection: TextDirection.ltr, ),
), TestSemantics(
TestSemantics( flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 15b', label: 'item 14b',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
TestSemantics( ]
label: 'Item 16a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 16b',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Item 17a',
textDirection: TextDirection.ltr,
), ),
TestSemantics( TestSemantics(
label: 'item 17b', children: <TestSemantics>[
textDirection: TextDirection.ltr, TestSemantics(
label: 'Item 15a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 15b',
textDirection: TextDirection.ltr,
),
],
), ),
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], children: <TestSemantics>[
label: 'Item 18a', TestSemantics(
textDirection: TextDirection.ltr, label: 'Item 16a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 16b',
textDirection: TextDirection.ltr,
),
],
), ),
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], children: <TestSemantics>[
label: 'item 18b', TestSemantics(
textDirection: TextDirection.ltr, label: 'Item 17a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 17b',
textDirection: TextDirection.ltr,
),
],
), ),
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 19a', children: <TestSemantics>[
textDirection: TextDirection.ltr, TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 18a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 18b',
textDirection: TextDirection.ltr,
),
],
), ),
TestSemantics( TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden], flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 19b', children: <TestSemantics>[
textDirection: TextDirection.ltr, TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 19a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 19b',
textDirection: TextDirection.ltr,
),
],
), ),
], ],
), ),
...@@ -181,7 +216,7 @@ void main() { ...@@ -181,7 +216,7 @@ void main() {
slivers: <Widget>[ slivers: <Widget>[
SliverFixedExtentList( SliverFixedExtentList(
itemExtent: 200.0, itemExtent: 200.0,
delegate: SliverChildListDelegate(listChildren), delegate: SliverChildListDelegate(listChildren, addSemanticIndexes: false),
), ),
], ],
), ),
......
...@@ -48,6 +48,8 @@ class TestSemantics { ...@@ -48,6 +48,8 @@ class TestSemantics {
this.transform, this.transform,
this.textSelection, this.textSelection,
this.children = const <TestSemantics>[], this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>), }) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>), assert(actions is int || actions is List<SemanticsAction>),
...@@ -73,6 +75,8 @@ class TestSemantics { ...@@ -73,6 +75,8 @@ class TestSemantics {
this.transform, this.transform,
this.textSelection, this.textSelection,
this.children = const <TestSemantics>[], this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : id = 0, }) : id = 0,
assert(flags is int || flags is List<SemanticsFlag>), assert(flags is int || flags is List<SemanticsFlag>),
...@@ -109,6 +113,8 @@ class TestSemantics { ...@@ -109,6 +113,8 @@ class TestSemantics {
Matrix4 transform, Matrix4 transform,
this.textSelection, this.textSelection,
this.children = const <TestSemantics>[], this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>), }) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>), assert(actions is int || actions is List<SemanticsAction>),
...@@ -200,6 +206,12 @@ class TestSemantics { ...@@ -200,6 +206,12 @@ class TestSemantics {
/// parent). /// parent).
final Matrix4 transform; 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; final TextSelection textSelection;
static Matrix4 _applyRootChildScale(Matrix4 transform) { static Matrix4 _applyRootChildScale(Matrix4 transform) {
...@@ -270,6 +282,12 @@ class TestSemantics { ...@@ -270,6 +282,12 @@ class TestSemantics {
if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) { 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}].'); 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; final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (children.length != childrenCount) if (children.length != childrenCount)
return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $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 { ...@@ -322,6 +340,8 @@ class TestSemantics {
buf.writeln('$indent textDirection: $textDirection,'); buf.writeln('$indent textDirection: $textDirection,');
if (textSelection?.isValid == true) if (textSelection?.isValid == true)
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],'); buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (scrollIndex != null)
buf.writeln('$indent scrollIndex: $scrollIndex,');
if (rect != null) if (rect != null)
buf.writeln('$indent rect: $rect,'); buf.writeln('$indent rect: $rect,');
if (transform != null) if (transform != null)
......
...@@ -17,7 +17,7 @@ void main() { ...@@ -17,7 +17,7 @@ void main() {
child: CustomScrollView( child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
SliverFillViewport( SliverFillViewport(
delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false), delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false, addSemanticIndexes: false),
), ),
], ],
), ),
......
...@@ -495,6 +495,8 @@ void main() { ...@@ -495,6 +495,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
textSelection: null, textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null, scrollPosition: null,
scrollExtentMax: null, scrollExtentMax: null,
scrollExtentMin: 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