Implicit scrolling for pageview (#45598)

This commit is contained in:
Dan Field 2019-11-26 14:12:34 -08:00 committed by GitHub
parent 7d8f82051b
commit 2d3d220988
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 6 deletions

View file

@ -416,6 +416,25 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
}
}
class _ForceImplicitScrollPhysics extends ScrollPhysics {
const _ForceImplicitScrollPhysics({
@required this.allowImplicitScrolling,
ScrollPhysics parent,
}) : assert(allowImplicitScrolling != null),
super(parent: parent);
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics ancestor) {
return _ForceImplicitScrollPhysics(
allowImplicitScrolling: allowImplicitScrolling,
parent: buildParent(ancestor),
);
}
@override
final bool allowImplicitScrolling;
}
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
@ -512,6 +531,13 @@ class PageView extends StatefulWidget {
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the page view, instead of just
/// those children that are actually visible.
///
/// {@template flutter.widgets.pageView.allowImplicitScrolling}
/// The [allowImplicitScrolling] parameter must not be null. If true, the
/// [PageView] will participate in accessibility scrolling more like a
/// [ListView], where implicit scroll actions will move to the next page
/// rather than into the contents of the [PageView].
/// {@endtemplate}
PageView({
Key key,
this.scrollDirection = Axis.horizontal,
@ -522,7 +548,9 @@ class PageView extends StatefulWidget {
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
}) : controller = controller ?? _defaultPageController,
this.allowImplicitScrolling = false,
}) : assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildListDelegate(children),
super(key: key);
@ -542,6 +570,8 @@ class PageView extends StatefulWidget {
/// [PageView.builder] by default does not support child reordering. If
/// you are planning to change child order at a later time, consider using
/// [PageView] or [PageView.custom].
///
/// {@macro flutter.widgets.pageView.allowImplicitScrolling}
PageView.builder({
Key key,
this.scrollDirection = Axis.horizontal,
@ -553,7 +583,9 @@ class PageView extends StatefulWidget {
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
this.dragStartBehavior = DragStartBehavior.start,
}) : controller = controller ?? _defaultPageController,
this.allowImplicitScrolling = false,
}) : assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
super(key: key);
@ -637,6 +669,8 @@ class PageView extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// {@macro flutter.widgets.pageView.allowImplicitScrolling}
PageView.custom({
Key key,
this.scrollDirection = Axis.horizontal,
@ -647,10 +681,25 @@ class PageView extends StatefulWidget {
this.onPageChanged,
@required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
}) : assert(childrenDelegate != null),
assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
super(key: key);
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
///
/// With this flag set to false, when accessibility focus reaches the end of
/// the current page and the user attempts to move it to the next element, the
/// focus will traverse to the next widget outside of the page view.
///
/// With this flag set to true, when accessibility focus reaches the end of
/// the current page and user attempts to move it to the next element, focus
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
@ -731,9 +780,11 @@ class _PageViewState extends State<PageView> {
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = widget.pageSnapping
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics)
: widget.physics;
: widget.physics);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
@ -754,7 +805,11 @@ class _PageViewState extends State<PageView> {
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
cacheExtent: 0.0,
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
slivers: <Widget>[
@ -777,5 +832,6 @@ class _PageViewState extends State<PageView> {
description.add(DiagnosticsProperty<PageController>('controller', widget.controller, showName: false));
description.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics, showName: false));
description.add(FlagProperty('pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled'));
description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
}
}

View file

@ -259,7 +259,7 @@ class ScrollPhysics {
double get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
/// Whether a viewport is allowed to change its scroll position implicitly in
/// responds to a call to [RenderObject.showOnScreen].
/// response to a call to [RenderObject.showOnScreen].
///
/// [RenderObject.showOnScreen] is for example used to bring a text field
/// fully on screen after it has received focus. This property controls

View file

@ -784,4 +784,46 @@ void main() {
pageController.position.jumpTo(799.99999999999);
expect(pageController.page, 1);
});
testWidgets('PageView can participate in a11y scrolling', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final PageController controller = PageController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: PageView(
controller: controller,
children: List<Widget>.generate(4, (int i) {
return Semantics(
child: Text('Page #$i'),
container: true,
);
}),
allowImplicitScrolling: true,
),
));
expect(controller.page, 0);
expect(semantics, includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling]));
expect(semantics, includesNodeWith(label: 'Page #0'));
expect(semantics, includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
expect(semantics, isNot(includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
expect(semantics, isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(semantics, includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
expect(semantics, includesNodeWith(label: 'Page #1'));
expect(semantics, includesNodeWith(label: 'Page #2', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
expect(semantics, isNot(includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
controller.nextPage(duration: const Duration(milliseconds: 150), curve: Curves.ease);
await tester.pumpAndSettle();
expect(semantics, isNot(includesNodeWith(label: 'Page #0', flags: <SemanticsFlag>[SemanticsFlag.isHidden])));
expect(semantics, includesNodeWith(label: 'Page #1', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
expect(semantics, includesNodeWith(label: 'Page #2'));
expect(semantics, includesNodeWith(label: 'Page #3', flags: <SemanticsFlag>[SemanticsFlag.isHidden]));
semantics.dispose();
});
}