Skip to content

Basic scroll semantics support - #21764

Merged
jonahwilliams merged 33 commits into
flutter:masterfrom
jonahwilliams:basic_scrolling_semantics
Oct 9, 2018
Merged

jonahwilliams merged 33 commits into
flutter:masterfrom
jonahwilliams:basic_scrolling_semantics

Conversation

@jonahwilliams

@jonahwilliams jonahwilliams commented Sep 12, 2018 •

Copy link
Copy Markdown
Contributor

This change requires an engine roll that hasn't happened yet and updates to several dozen tests.


Note: The concept of "List" as given here is what a user of an app would think of a list.

Given scrollIndex, the first visible child of a list and scrollChildren, the total number of items in a list, TalkBack and VoiceOver will provide additional context to the user:

Showing items ${scrollIndex} to ${scrollIndex + visibleCount} of ${scrollChildren}

Or

Showing item ${scrollIndex} of ${scrollChildren}

This change requires a corresponding engine change which adds scrollIndex and scrollChildren to the semantics node. Having done that, what remains is to correctly pass this information through.

So given the three pieces of information needed above, we can start by having Scrollable/ScrollView take "scrollChildren" as a parameter. I called this field "semanticChildren" to distinguish it. In the case of ListView and other subclasses, we can usually infer this value from the length of children or item count passed - but we always allow it to be specified just in case they are different. For the CustomScrollView, the value always needs to be provided.

The next piece of information we need to pipe through is the first visible index. We already know what semantic node is visible by the time assembleSemanticNodes is called in the Scrollable, but at this point we have no to figure out what the index of this was, since slivers are lazily rendered. To solve this, I added IndexedChildSemantics/RenderIndexedChildSemantics which allows tagging a semantics node with indexInParent. Then we can simply read this value off of the semantic node.

It is the responsibility of specific subclasses to handle tagging correctly. For the SliverChildListDelegate and SliverChildBuilderDelegate classes I added three parameters to control this:

  • addSemanticIndexes - defaults to true, whether to apply indexedChildSemantics. This would allow users to provide their own if necessary.

  • semanticIndexOffset - defaults to 0, an offset to apply to each index added to a given child. For example, if you have a CustomScrollView with two SliverLists, you can give the second a semanticIndexOffset equal to the length of the first.

  • semanticIndexCallback - defaults to always returning an index. A callback which takes a widget and the local index and provides a new index, with null based short circuit. This allows ListView.separated ti ignore odd indexes.

Together all of these additions allow the delegates to pass their builder's index to the child semantics.

The final piece of information, the number of visible children, is handled automatically in the engine by checking for the last child semantics node that isn't hidden.


For simple cases, like ListView/ListView.builder/ListView.separated no change needs to be made.

Fixes #10595

@jonahwilliams jonahwilliams changed the title [WIP] basic scroll semantics support Basic scroll semantics support Sep 26, 2018
Comment thread packages/flutter/lib/src/semantics/semantics.dart Outdated
/// if this node represents a text field.
final TextSelection textSelection;

/// The count of children of a scroll node which contribute semantics.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Total count? Visible count? That's the guidance for the case when the child count is virtually unlimited?

Rect parentPaintClipRect;

/// The index of this node with respect to its parent, including any hidden
/// siblings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I do not understand this dartdoc :)

What does "with respect to" mean when talking about an index? And how can siblings be included with respect to the parent? 😕

Is being hidden determined by some property? Could you link to that property from the doc?

int _flags = _kEmptyConfig._flags;

bool _hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;
/// Whether the semantics node currently has a given [SemanticsFlag].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd prefer to say "Whether this node currently..."

TextSelection _textSelection;

/// The count of children of a scroll node which contribute semantics.
int get scrollChildren => _scrollChildren;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: scrollChildren sounds like the getter returns the children, while in fact it returns count. How about call it scrollChildCount?

/// {@macro flutter.rendering.viewport.cacheExtent}
final double cacheExtent;

/// The number of children which will contribute semantic information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "that will"? I think when you say "which" you refer to "the number", not "children".

/// exclusion.
final bool excludeFromSemantics;

/// The number of children which will contribute semantic information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "which" => "that"

/// Whether to wrap each child in an [IndexedChildSemantics].
///
/// Defaults to true.
final bool addSemanticIndexes;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property reads like a method name. isAddSemanticIndexes? Also, do we prefer "indexes" or "indices"? We should probably be consistent with the rest of the API.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ag -Q indices | wc -l -> 36
ag -Q indexes | wc -l -> 47

Guess we are agnostic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this matches the naming of the other config

/// Whether to wrap each child in an [IndexedChildSemantics].
///
/// Defaults to true.
final bool addSemanticIndexes;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

this.textSelection,
this.children = const <TestSemantics>[],
this.scrollIndex,
this.scrollChildren,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea I had in the past, but never got to implement, was to make this more flexible by taking Matcher as a value. Then you could use an any matcher to indicate that you don't care about the value. I don't know if it's helpful in this case.

Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
semanticChildren: 50,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semanticsChildrenCount? children sounds like I can pass in the actual children?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about semanticChildrenCount vs semanticsChildrenCount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber nitty nit: Almost everywhere else we say "childCount" rather than "childrenCount". If we insist on "children", then perhaps we should say "numberOfChildren".

/// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the amount is infinity?

Comment thread packages/flutter/lib/src/widgets/scrollable.dart

@goderbauer goderbauer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere (maybe on the CutomScrollWidget) there should be a section # Accessibility documenting how to make a custom scroller accessible. Currently, that documentation is kinda all over the place.

/// semantics. For example, the [ScrollView] uses the index of the first
/// visible child semantics node to determine the
/// [SemanticsConfiguration.scrollIndex].
class IndexedChildSemantics extends SingleChildRenderObjectWidget {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an example of where and when somebody should use this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you address this comment, too?

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be included in this PR?

/// semantics. For example, the [ScrollView] uses the index of the first
/// visible child semantics node to determine the
/// [SemanticsConfiguration.scrollIndex].
class IndexedChildSemantics extends SingleChildRenderObjectWidget {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you address this comment, too?

bool get allowImplicitScrolling => true;

/// Generates a semantic scroll value, or null if none should be created.
String semanticScrollValue({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should have more documentation. In what cases should this return a non-null value? What's an example value this should return in that case?

Comment thread packages/flutter/lib/src/widgets/sliver.dart
Comment thread packages/flutter/lib/src/widgets/sliver.dart
bool get allowImplicitScrolling => true;

/// Generates a semantic scroll value, or null if none should be created.
String semanticScrollValue({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that this is not overwritten anywhere with an actual implementation? Where is this one used?

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

Don't worry I'm still working on this! Will update when it is ready for review again... need to remember what I was doing.

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

I believe I've addressed all of the comments wrt documentation. Whenever you have a chance PTAL - not tonight of course :)

Jonah Williams and others added 2 commits October 4, 2018 10:15

@yjbanov yjbanov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if LGT @goderbauer

: separatorBuilder(context, itemIndex);
},
childCount: math.max(0, itemCount * 2 - 1),
childCount: math.max(0, itemCount * 2 - 1), // same calculation as in semanticChildrenCount below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: semanticChildCount (in the comment)

Comment thread packages/flutter/lib/src/widgets/basic.dart

@goderbauer goderbauer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some minor nits.

Comment thread examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart Outdated
Comment thread packages/flutter/lib/src/widgets/basic.dart
Comment thread packages/flutter/lib/src/widgets/scroll_view.dart Outdated
/// )
/// ```
///
/// ## Accessibility

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for this explanation.

Comment thread packages/flutter/lib/src/widgets/scroll_view.dart Outdated
Comment thread packages/flutter/lib/src/widgets/sliver.dart Outdated
Comment thread packages/flutter/lib/src/widgets/sliver.dart Outdated
@jonahwilliams

Copy link
Copy Markdown
Contributor Author

I just added some more sample code sections to the SliverDelegates - should I add identical code + comments to each or just a note that says see [Other]?

@goderbauer

Copy link
Copy Markdown
Member

"See other" sounds fine to me.

@goderbauer

Copy link
Copy Markdown
Member

❤️ for all the samples!

@jonahwilliams
jonahwilliams merged commit fb7a593 into flutter:master Oct 9, 2018
@jonahwilliams
jonahwilliams deleted the basic_scrolling_semantics branch October 9, 2018 21:16
@rsnider19

Copy link
Copy Markdown

@jonahwilliams I am trying to determine when an item is scrolled off of the screen, and this seems like what I need, but I don't understand how get scrollIndex. Is this something that we have access to, or is it just meant for the accessibility frameworks?

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

This is probably not the API you are looking for, since it requires an accessibility service to be running.

@rsnider19

Copy link
Copy Markdown

Gotcha, thanks!

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scrollviews need to announce which items of how many are shown

6 participants