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:
......
...@@ -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.
......
...@@ -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