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

Basic scroll semantics support (#21764)

parent 59f1f1b6
......@@ -30,6 +30,8 @@ const List<String> coolColorNames = <String>[
'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre',
];
const int _kChildCount = 50;
class CupertinoNavigationDemo extends StatelessWidget {
CupertinoNavigationDemo()
: colorItems = List<Color>.generate(50, (int index) {
......@@ -146,6 +148,7 @@ class CupertinoDemoTab1 extends StatelessWidget {
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
semanticChildCount: _kChildCount,
slivers: <Widget>[
CupertinoSliverNavigationBar(
trailing: trailingButtons,
......@@ -163,12 +166,12 @@ class CupertinoDemoTab1 extends StatelessWidget {
(BuildContext context, int index) {
return Tab1RowItem(
index: index,
lastItem: index == 49,
lastItem: index == _kChildCount - 1,
color: colorItems[index],
colorName: colorNameItems[index],
);
},
childCount: 50,
childCount: _kChildCount,
),
),
),
......
......@@ -91,6 +91,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
},
),
body: CustomScrollView(
semanticChildCount: widget.recipes.length,
slivers: <Widget>[
_buildAppBar(context, statusBarHeight),
_buildBody(context, statusBarHeight),
......
......@@ -4330,6 +4330,49 @@ class RenderExcludeSemantics extends RenderProxyBox {
}
}
/// A render objects that annotates semantics with an index.
///
/// Certain widgets will automatically provide a child index for building
/// semantics. For example, the [ScrollView] uses the index of the first
/// visible child semantics node to determine the
/// [SemanticsConfiguration.scrollIndex].
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
class RenderIndexedSemantics extends RenderProxyBox {
/// Creates a render object that annotates the child semantics with an index.
RenderIndexedSemantics({
RenderBox child,
@required int index,
}) : assert(index != null),
_index = index,
super(child);
/// The index used to annotated child semantics.
int get index => _index;
int _index;
set index(int value) {
if (value == index)
return;
_index = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
config.indexInParent = index;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<int>('index', index));
}
}
/// Provides an anchor for a [RenderFollowerLayer].
///
/// See also:
......
......@@ -5553,6 +5553,64 @@ class ExcludeSemantics extends SingleChildRenderObjectWidget {
}
}
/// A widget that annotates the child semantics with an index.
///
/// Semantic indexes are used by TalkBack/Voiceover to make announcements about
/// the current scroll state. Certain widgets like the [ListView] will
/// automatically provide a child index for building semantics. A user may wish
/// to manually provide semanitc indexes if not all child of the scrollable
/// contribute semantics.
///
/// ## Sample code
///
/// The example below handles spacers in a scrollable that don't contribute
/// semantics. The automatic indexes would give the spaces a semantic index,
/// causing scroll announcements to erroneously state that there are four items
/// visible.
///
/// ```dart
/// ListView(
/// addSemanticIndexes: false,
/// semanticChildCount: 2,
/// children: const <Widget>[
/// IndexedSemantics(index: 0, child: Text('First')),
/// Spacer(),
/// IndexedSemantics(index: 1, child: Text('Second')),
/// Spacer(),
/// ],
/// )
/// ```
///
/// See also:
/// * [CustomScrollView], for an explaination of index semantics.
class IndexedSemantics extends SingleChildRenderObjectWidget {
/// Creates a widget that annotated the first child semantics node with an index.
///
/// [index] must not be null.
const IndexedSemantics({
Key key,
@required this.index,
Widget child,
}) : assert(index != null),
super(key: key, child: child);
/// The index used to annotate the first child semantics node.
final int index;
@override
RenderIndexedSemantics createRenderObject(BuildContext context) => RenderIndexedSemantics(index: index);
@override
void updateRenderObject(BuildContext context, RenderIndexedSemantics renderObject) {
renderObject.index = index;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<int>('index', index));
}
}
/// A widget that builds its child.
///
/// Useful for attaching a key to an existing widget.
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
......@@ -79,6 +80,7 @@ class Scrollable extends StatefulWidget {
this.physics,
@required this.viewportBuilder,
this.excludeFromSemantics = false,
this.semanticChildCount,
}) : assert(axisDirection != null),
assert(viewportBuilder != null),
assert(excludeFromSemantics != null),
......@@ -161,6 +163,23 @@ class Scrollable extends StatefulWidget {
/// exclusion.
final bool excludeFromSemantics;
/// The number of children that will contribute semantic information.
///
/// The value will be null if the number of children is unknown or unbounded.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [new ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided.
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
final int semanticChildCount;
/// The axis along which the scroll view scrolls.
///
/// Determined by the [axisDirection].
......@@ -509,6 +528,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
child: result,
position: position,
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? false,
semanticChildCount: widget.semanticChildCount,
);
}
......@@ -541,17 +561,20 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
Key key,
@required this.position,
@required this.allowImplicitScrolling,
@required this.semanticChildCount,
Widget child
}) : assert(position != null), super(key: key, child: child);
final ScrollPosition position;
final bool allowImplicitScrolling;
final int semanticChildCount;
@override
_RenderScrollSemantics createRenderObject(BuildContext context) {
return _RenderScrollSemantics(
position: position,
allowImplicitScrolling: allowImplicitScrolling,
semanticChildCount: semanticChildCount,
);
}
......@@ -559,7 +582,8 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
renderObject
..allowImplicitScrolling = allowImplicitScrolling
..position = position;
..position = position
..semanticChildCount = semanticChildCount;
}
}
......@@ -567,9 +591,11 @@ class _RenderScrollSemantics extends RenderProxyBox {
_RenderScrollSemantics({
@required ScrollPosition position,
@required bool allowImplicitScrolling,
@required int semanticChildCount,
RenderBox child,
}) : _position = position,
_allowImplicitScrolling = allowImplicitScrolling,
_semanticChildCount = semanticChildCount,
assert(position != null), super(child) {
position.addListener(markNeedsSemanticsUpdate);
}
......@@ -597,6 +623,15 @@ class _RenderScrollSemantics extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
int get semanticChildCount => _semanticChildCount;
int _semanticChildCount;
set semanticChildCount(int value) {
if (value == semanticChildCount)
return;
_semanticChildCount = value;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -606,7 +641,8 @@ class _RenderScrollSemantics extends RenderProxyBox {
..hasImplicitScrolling = allowImplicitScrolling
..scrollPosition = _position.pixels
..scrollExtentMax = _position.maxScrollExtent
..scrollExtentMin = _position.minScrollExtent;
..scrollExtentMin = _position.minScrollExtent
..scrollChildCount = semanticChildCount;
}
}
......@@ -624,15 +660,20 @@ class _RenderScrollSemantics extends RenderProxyBox {
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
int firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling))
excluded.add(child);
else
else {
if (!child.hasFlag(SemanticsFlag.isHidden))
firstVisibleIndex ??= child.indexInParent;
included.add(child);
}
}
config.scrollIndex = firstVisibleIndex;
node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
}
......
......@@ -16,6 +16,25 @@ export 'package:flutter/rendering.dart' show
SliverGridDelegateWithFixedCrossAxisCount,
SliverGridDelegateWithMaxCrossAxisExtent;
// Examples can assume:
// SliverGridDelegateWithMaxCrossAxisExtent _gridDelegate;
/// A callback which produces a semantic index given a widget and the local index.
///
/// Return a null value to prevent a widget from receiving an index.
///
/// A semantic index is used to tag child semantic nodes for accessibility
/// announcements in scroll view.
///
/// See also:
///
/// * [CustomScrollView], for an explanation of scroll semantics.
/// * [SliverChildBuilderDelegate], for an explanation of how this is used to
/// generate indexes.
typedef SemanticIndexCallback = int Function(Widget widget, int localIndex);
int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
/// A delegate that supplies children for slivers.
///
/// Many slivers lazily construct their box children to avoid creating more
......@@ -188,10 +207,96 @@ abstract class SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// ## Accessibility
///
/// The [CustomScrollView] requires that its semantic children are annotated
/// using [IndexedSemantics]. This is done by default in the delegate with
/// the `addSemanticIndexes` parameter set to true.
///
/// If multiple delegates are used in a single scroll view, then the indexes
/// will not be correct by default. The `semanticIndexOffset` can be used to
/// offset the semantic indexes of each delegate so that the indexes are
/// monotonically increasing. For example, if a scroll view contains two
/// delegates where the first has 10 children contributing semantics, then the
/// second delegate should offset its children by 10.
///
/// ## Sample code
///
/// This sample code shows how to use `semanticIndexOffset` to handle multiple
/// delegates in a single scroll view.
///
/// ```dart
/// CustomScrollView(
/// semanticChildCount: 4,
/// slivers: <Widget>[
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return Text('...');
/// },
/// childCount: 2,
/// ),
/// ),
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return Text('...');
/// },
/// childCount: 2,
/// semanticIndexOffset: 2,
/// ),
/// ),
/// ],
/// )
/// ```
///
/// In certain cases, only a subset of child widgets should be annotated
/// with a semantic index. For example, in [new ListView.separated()] the
/// separators do not have an index assocaited with them. This is done by
/// providing a `semanticIndexCallback` which returns null for separators
/// indexes and rounds the non-separator indexes down by half.
///
/// ## Sample code
///
/// This sample code shows how to use `semanticIndexCallback` to handle
/// annotating a subset of child nodes with a semantic index. There is
/// a [Spacer] widget at odd indexes which should not have a semantic
/// index.
///
/// ```dart
/// CustomScrollView(
/// semanticChildCount: 5,
/// slivers: <Widget>[
/// SliverGrid(
/// gridDelegate: _gridDelegate,
/// delegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// if (index.isEven) {
/// return Text('...');
/// }
/// return Spacer();
/// },
/// semanticIndexCallback: (Widget widget, int localIndex) {
/// if (localIndex.isEven) {
/// return localIndex ~/ 2;
/// }
/// return null;
/// },
/// childCount: 10,
/// ),
/// ),
/// ],
/// )
/// ```
///
/// See also:
///
/// * [SliverChildListDelegate], which is a delegate that has an explicit list
/// of children.
/// * [IndexedSemantics], for an example of manually annotating child nodes
/// with semantic indexes.
class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// builder callback.
......@@ -203,6 +308,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
this.childCount,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(builder != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
......@@ -250,6 +358,27 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by wrapping the correct child widgets in an
/// indexed child semantics widget.
///
/// Defaults to true.
final bool addSemanticIndexes;
/// An initial offset to add to the semantic indexes generated by this widget.
///
/// Defaults to zero.
final int semanticIndexOffset;
/// A [SemanticIndexCallback] which is used when [addSemanticIndexes] is true.
///
/// Defaults to providing an index for each widget.
final SemanticIndexCallback semanticIndexCallback;
@override
Widget build(BuildContext context, int index) {
assert(builder != null);
......@@ -260,6 +389,11 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
return null;
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
......@@ -297,6 +431,28 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// ## Accessibility
///
/// The [CustomScrollView] requires that its semantic children are annotated
/// using [IndexedSemantics]. This is done by default in the delegate with
/// the `addSemanticIndexes` parameter set to true.
///
/// If multiple delegates are used in a single scroll view, then the indexes
/// will not be correct by default. The `semanticIndexOffset` can be used to
/// offset the semantic indexes of each delegate so that the indexes are
/// monotonically increasing. For example, if a scroll view contains two
/// delegates where the first has 10 children contributing semantics, then the
/// second delegate should offset its children by 10.
///
/// In certain cases, only a subset of child widgets should be annotated
/// with a semantic index. For example, in [new ListView.separated()] the
/// separators do not have an index assocaited with them. This is done by
/// providing a `semanticIndexCallback` which returns null for separators
/// indexes and rounds the non-separator indexes down by half.
///
/// See [SliverChildBuilderDelegate] for sample code using
/// `semanticIndexOffset` and `semanticIndexCallback`.
///
/// See also:
///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
......@@ -311,6 +467,9 @@ class SliverChildListDelegate extends SliverChildDelegate {
this.children, {
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
this.semanticIndexOffset = 0,
}) : assert(children != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
......@@ -340,6 +499,27 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by wrapping the correct child widgets in an
/// indexed child semantics widget.
///
/// Defaults to true.
final bool addSemanticIndexes;
/// An initial offset to add to the semantic indexes generated by this widget.
///
/// Defaults to zero.
final int semanticIndexOffset;
/// A [SemanticIndexCallback] which is used when [addSemanticIndexes] is true.
///
/// Defaults to providing an index for each widget.
final SemanticIndexCallback semanticIndexCallback;
/// The widgets to display.
final List<Widget> children;
......@@ -352,6 +532,11 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(child != null);
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
......
......@@ -348,6 +348,8 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
......@@ -438,6 +440,8 @@ void main() {
' hint: ""\n'
' textDirection: null\n'
' sortKey: null\n'
' scrollChildren: null\n'
' scrollIndex: null\n'
' scrollExtentMin: null\n'
' scrollPosition: null\n'
' scrollExtentMax: null\n'
......
......@@ -70,6 +70,7 @@ void tests({ @required bool impliedMode }) {
child: ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible
cacheExtent: 0.0,
children: generateList(const Placeholder(), impliedMode: impliedMode),
......@@ -117,6 +118,7 @@ void tests({ @required bool impliedMode }) {
child: ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: generateList(
Container(height: 12.3, child: const Placeholder()), // about 50 widgets visible
......@@ -166,6 +168,7 @@ void tests({ @required bool impliedMode }) {
child: GridView.count(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
addSemanticIndexes: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
cacheExtent: 0.0,
......@@ -222,6 +225,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -305,6 +309,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -360,6 +365,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -423,6 +429,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
cacheExtent: 0.0,
children: <Widget>[
AutomaticKeepAlive(
......@@ -468,6 +475,7 @@ void main() {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
addSemanticIndexes: false,
itemCount: 50,
itemBuilder: (BuildContext context, int index){
if (index == 0){
......@@ -501,6 +509,7 @@ void main() {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
addSemanticIndexes: false,
itemCount: 250,
itemBuilder: (BuildContext context, int index){
if (index % 2 == 0){
......
......@@ -1658,6 +1658,7 @@ void main() {
await tester.pumpWidget(MaterialApp(
home: ListView(
scrollDirection: Axis.horizontal,
addSemanticIndexes: false,
children: <Widget>[
DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
......
......@@ -48,6 +48,7 @@ Widget buildListView(Axis scrollDirection, { bool reverse = false, bool shrinkWr
child: ListView(
scrollDirection: scrollDirection,
reverse: reverse,
addSemanticIndexes: false,
shrinkWrap: shrinkWrap,
children: <Widget>[
Container(key: const ValueKey<int>(0), width: 200.0, height: 200.0),
......
......@@ -50,6 +50,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()),
),
......@@ -97,6 +98,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
children: generateList(Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
),
),
......@@ -143,6 +145,7 @@ void main() {
cacheExtent: 0.0,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
children: generateList(Container(child: const Placeholder())),
......@@ -190,6 +193,7 @@ void main() {
child: ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()),
),
......
......@@ -308,6 +308,7 @@ void main() {
textDirection: TextDirection.ltr,
child: ListView(
addAutomaticKeepAlives: false,
addSemanticIndexes: false,
children: <Widget>[
Container(height: 100.0),
Container(height: 100.0),
......
......@@ -42,6 +42,7 @@ void main() {
data: const MediaQueryData(),
child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 3000.0),
semanticChildCount: 30,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(listChildren),
......@@ -62,6 +63,8 @@ void main() {
TestSemantics(
children: <TestSemantics>[
TestSemantics(
scrollIndex: 15,
scrollChildren: 30,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
......@@ -69,67 +72,99 @@ void main() {
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 13a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 13b',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 14a',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 13a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 13b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 14b',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Item 15a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 15b',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Item 16a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 16b',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Item 17a',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 14a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 14b',
textDirection: TextDirection.ltr,
),
]
),
TestSemantics(
label: 'item 17b',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
label: 'Item 15a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 15b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 18a',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
label: 'Item 16a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 16b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 18b',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
label: 'Item 17a',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'item 17b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 19a',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'Item 18a',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 18b',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
label: 'item 19b',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
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() {
slivers: <Widget>[
SliverFixedExtentList(
itemExtent: 200.0,
delegate: SliverChildListDelegate(listChildren),
delegate: SliverChildListDelegate(listChildren, addSemanticIndexes: false),
),
],
),
......
......@@ -48,6 +48,8 @@ class TestSemantics {
this.transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
......@@ -73,6 +75,8 @@ class TestSemantics {
this.transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : id = 0,
assert(flags is int || flags is List<SemanticsFlag>),
......@@ -109,6 +113,8 @@ class TestSemantics {
Matrix4 transform,
this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,
Iterable<SemanticsTag> tags,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
......@@ -200,6 +206,12 @@ class TestSemantics {
/// parent).
final Matrix4 transform;
/// The index of the first visible semantic node within a scrollable.
final int scrollIndex;
/// The total number of semantic nodes within a scrollable.
final int scrollChildren;
final TextSelection textSelection;
static Matrix4 _applyRootChildScale(Matrix4 transform) {
......@@ -270,6 +282,12 @@ class TestSemantics {
if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
return fail('expected node id $id to have textSelection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
}
if (scrollIndex != null && scrollIndex != nodeData.scrollIndex) {
return fail('expected node id $id to have scrollIndex $scrollIndex but found scrollIndex ${nodeData.scrollIndex}.');
}
if (scrollChildren != null && scrollChildren != nodeData.scrollChildCount) {
return fail('expected node id $id to have scrollIndex $scrollChildren but found scrollIndex ${nodeData.scrollChildCount}.');
}
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (children.length != childrenCount)
return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');
......@@ -322,6 +340,8 @@ class TestSemantics {
buf.writeln('$indent textDirection: $textDirection,');
if (textSelection?.isValid == true)
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (scrollIndex != null)
buf.writeln('$indent scrollIndex: $scrollIndex,');
if (rect != null)
buf.writeln('$indent rect: $rect,');
if (transform != null)
......
......@@ -17,7 +17,7 @@ void main() {
child: CustomScrollView(
slivers: <Widget>[
SliverFillViewport(
delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false),
delegate: SliverChildListDelegate(children, addAutomaticKeepAlives: false, addSemanticIndexes: false),
),
],
),
......
......@@ -495,6 +495,8 @@ void main() {
textDirection: TextDirection.ltr,
rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
textSelection: null,
scrollIndex: null,
scrollChildCount: null,
scrollPosition: null,
scrollExtentMax: null,
scrollExtentMin: null,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment