mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Implicit scrolling for pageview (#45598)
This commit is contained in:
parent
7d8f82051b
commit
2d3d220988
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue