395 lines
13 KiB
Dart
395 lines
13 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart' hide PageView;
|
|
|
|
/// This is copy-pasted from the Flutter framework with a support added for building
|
|
/// pages off screen using [Viewport.cacheExtents] and a [LayoutBuilder]
|
|
///
|
|
/// Based on commit 3932ffb1cd5dfa0c3891c60977ee4f9cd70ade66 on channel dev
|
|
|
|
// Having this global (mutable) page controller is a bit of a hack. We need it
|
|
// to plumb in the factory for _PagePosition, but it will end up accumulating
|
|
// a large list of scroll positions. As long as you don't try to actually
|
|
// control the scroll positions, everything should be fine.
|
|
final PageController _defaultPageController = PageController();
|
|
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
|
|
|
/// A scrollable list that works page by page.
|
|
///
|
|
/// Each child of a page view is forced to be the same size as the viewport.
|
|
///
|
|
/// You can use a [PageController] to control which page is visible in the view.
|
|
/// In addition to being able to control the pixel offset of the content inside
|
|
/// the [PageView], a [PageController] also lets you control the offset in terms
|
|
/// of pages, which are increments of the viewport size.
|
|
///
|
|
/// The [PageController] can also be used to control the
|
|
/// [PageController.initialPage], which determines which page is shown when the
|
|
/// [PageView] is first constructed, and the [PageController.viewportFraction],
|
|
/// which determines the size of the pages as a fraction of the viewport size.
|
|
///
|
|
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [PageController], which controls which page is visible in the view.
|
|
/// * [SingleChildScrollView], when you need to make a single child scrollable.
|
|
/// * [ListView], for a scrollable list of boxes.
|
|
/// * [GridView], for a scrollable grid of boxes.
|
|
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
|
|
/// the scroll position without using a [ScrollController].
|
|
class ExtentsPageView extends StatefulWidget {
|
|
/// Creates a scrollable list that works page by page from an explicit [List]
|
|
/// of widgets.
|
|
///
|
|
/// This constructor is appropriate for page views with a small number of
|
|
/// 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.
|
|
ExtentsPageView({
|
|
Key key,
|
|
this.scrollDirection = Axis.horizontal,
|
|
this.reverse = false,
|
|
PageController controller,
|
|
this.physics,
|
|
this.pageSnapping = true,
|
|
this.onPageChanged,
|
|
List<Widget> children = const <Widget>[],
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
}) : controller = controller ?? _defaultPageController,
|
|
childrenDelegate = SliverChildListDelegate(children),
|
|
extents = children.length,
|
|
super(key: key);
|
|
|
|
/// Creates a scrollable list that works page by page using widgets that are
|
|
/// created on demand.
|
|
///
|
|
/// This constructor is appropriate for page views with a large (or infinite)
|
|
/// number of children because the builder is called only for those children
|
|
/// that are actually visible.
|
|
///
|
|
/// Providing a non-null [itemCount] lets the [PageView] compute the maximum
|
|
/// scroll extent.
|
|
///
|
|
/// [itemBuilder] will be called only with indices greater than or equal to
|
|
/// zero and less than [itemCount].
|
|
///
|
|
/// [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].
|
|
ExtentsPageView.builder({
|
|
Key key,
|
|
this.scrollDirection = Axis.horizontal,
|
|
this.reverse = false,
|
|
PageController controller,
|
|
this.physics,
|
|
this.pageSnapping = true,
|
|
this.onPageChanged,
|
|
@required IndexedWidgetBuilder itemBuilder,
|
|
int itemCount,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
}) : controller = controller ?? _defaultPageController,
|
|
childrenDelegate =
|
|
SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
|
|
extents = 0,
|
|
super(key: key);
|
|
|
|
ExtentsPageView.extents({
|
|
Key key,
|
|
this.extents = 1,
|
|
this.scrollDirection = Axis.horizontal,
|
|
this.reverse = false,
|
|
PageController controller,
|
|
this.physics,
|
|
this.pageSnapping = true,
|
|
this.onPageChanged,
|
|
@required IndexedWidgetBuilder itemBuilder,
|
|
int itemCount,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
}) : controller = controller ?? _defaultPageController,
|
|
childrenDelegate = SliverChildBuilderDelegate(
|
|
itemBuilder,
|
|
childCount: itemCount,
|
|
addAutomaticKeepAlives: false,
|
|
addRepaintBoundaries: false,
|
|
),
|
|
super(key: key);
|
|
|
|
/// Creates a scrollable list that works page by page with a custom child
|
|
/// model.
|
|
///
|
|
/// {@tool sample}
|
|
///
|
|
/// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child
|
|
/// reordering.
|
|
///
|
|
/// ```dart
|
|
/// class MyPageView extends StatefulWidget {
|
|
/// @override
|
|
/// _MyPageViewState createState() => _MyPageViewState();
|
|
/// }
|
|
///
|
|
/// class _MyPageViewState extends State<MyPageView> {
|
|
/// List<String> items = <String>['1', '2', '3', '4', '5'];
|
|
///
|
|
/// void _reverse() {
|
|
/// setState(() {
|
|
/// items = items.reversed.toList();
|
|
/// });
|
|
/// }
|
|
///
|
|
/// @override
|
|
/// Widget build(BuildContext context) {
|
|
/// return Scaffold(
|
|
/// body: SafeArea(
|
|
/// child: PageView.custom(
|
|
/// childrenDelegate: SliverChildBuilderDelegate(
|
|
/// (BuildContext context, int index) {
|
|
/// return KeepAlive(
|
|
/// data: items[index],
|
|
/// key: ValueKey<String>(items[index]),
|
|
/// );
|
|
/// },
|
|
/// childCount: items.length,
|
|
/// findChildIndexCallback: (Key key) {
|
|
/// final ValueKey valueKey = key;
|
|
/// final String data = valueKey.value;
|
|
/// return items.indexOf(data);
|
|
/// }
|
|
/// ),
|
|
/// ),
|
|
/// ),
|
|
/// bottomNavigationBar: BottomAppBar(
|
|
/// child: Row(
|
|
/// mainAxisAlignment: MainAxisAlignment.center,
|
|
/// children: <Widget>[
|
|
/// FlatButton(
|
|
/// onPressed: () => _reverse(),
|
|
/// child: Text('Reverse items'),
|
|
/// ),
|
|
/// ],
|
|
/// ),
|
|
/// ),
|
|
/// );
|
|
/// }
|
|
/// }
|
|
///
|
|
/// class KeepAlive extends StatefulWidget {
|
|
/// const KeepAlive({Key key, this.data}) : super(key: key);
|
|
///
|
|
/// final String data;
|
|
///
|
|
/// @override
|
|
/// _KeepAliveState createState() => _KeepAliveState();
|
|
/// }
|
|
///
|
|
/// class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin{
|
|
/// @override
|
|
/// bool get wantKeepAlive => true;
|
|
///
|
|
/// @override
|
|
/// Widget build(BuildContext context) {
|
|
/// super.build(context);
|
|
/// return Text(widget.data);
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
/// {@end-tool}
|
|
ExtentsPageView.custom({
|
|
Key key,
|
|
this.scrollDirection = Axis.horizontal,
|
|
this.reverse = false,
|
|
PageController controller,
|
|
this.physics,
|
|
this.pageSnapping = true,
|
|
this.onPageChanged,
|
|
@required this.childrenDelegate,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
}) : assert(childrenDelegate != null),
|
|
extents = 0,
|
|
controller = controller ?? _defaultPageController,
|
|
super(key: key);
|
|
|
|
/// The number of pages to build off screen.
|
|
///
|
|
/// For example, a value of `1` builds one page ahead and one page behind,
|
|
/// for a total of three built pages.
|
|
///
|
|
/// This is especially useful for making sure heavyweight widgets have a chance
|
|
/// to load off-screen before the user pulls it into the viewport.
|
|
final int extents;
|
|
|
|
/// The axis along which the page view scrolls.
|
|
///
|
|
/// Defaults to [Axis.horizontal].
|
|
final Axis scrollDirection;
|
|
|
|
/// Whether the page view scrolls in the reading direction.
|
|
///
|
|
/// For example, if the reading direction is left-to-right and
|
|
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
|
|
/// left to right when [reverse] is false and from right to left when
|
|
/// [reverse] is true.
|
|
///
|
|
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
|
|
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
|
/// when [reverse] is true.
|
|
///
|
|
/// Defaults to false.
|
|
final bool reverse;
|
|
|
|
/// An object that can be used to control the position to which this page
|
|
/// view is scrolled.
|
|
final PageController controller;
|
|
|
|
/// How the page view should respond to user input.
|
|
///
|
|
/// For example, determines how the page view continues to animate after the
|
|
/// user stops dragging the page view.
|
|
///
|
|
/// The physics are modified to snap to page boundaries using
|
|
/// [PageScrollPhysics] prior to being used.
|
|
///
|
|
/// Defaults to matching platform conventions.
|
|
final ScrollPhysics physics;
|
|
|
|
/// Set to false to disable page snapping, useful for custom scroll behavior.
|
|
final bool pageSnapping;
|
|
|
|
/// Called whenever the page in the center of the viewport changes.
|
|
final ValueChanged<int> onPageChanged;
|
|
|
|
/// A delegate that provides the children for the [PageView].
|
|
///
|
|
/// The [PageView.custom] constructor lets you specify this delegate
|
|
/// explicitly. The [PageView] and [PageView.builder] constructors create a
|
|
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
|
|
/// respectively.
|
|
final SliverChildDelegate childrenDelegate;
|
|
|
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
@override
|
|
_PageViewState createState() => _PageViewState();
|
|
}
|
|
|
|
class _PageViewState extends State<ExtentsPageView> {
|
|
int _lastReportedPage = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_lastReportedPage = widget.controller.initialPage;
|
|
}
|
|
|
|
AxisDirection _getDirection(BuildContext context) {
|
|
switch (widget.scrollDirection) {
|
|
case Axis.horizontal:
|
|
assert(debugCheckHasDirectionality(context));
|
|
final TextDirection textDirection = Directionality.of(context);
|
|
final AxisDirection axisDirection =
|
|
textDirectionToAxisDirection(textDirection);
|
|
return widget.reverse
|
|
? flipAxisDirection(axisDirection)
|
|
: axisDirection;
|
|
case Axis.vertical:
|
|
return widget.reverse ? AxisDirection.up : AxisDirection.down;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final AxisDirection axisDirection = _getDirection(context);
|
|
final ScrollPhysics physics = widget.pageSnapping
|
|
? _kPagePhysics.applyTo(widget.physics)
|
|
: widget.physics;
|
|
|
|
return NotificationListener<ScrollNotification>(
|
|
onNotification: (ScrollNotification notification) {
|
|
if (notification.depth == 0 &&
|
|
widget.onPageChanged != null &&
|
|
notification is ScrollUpdateNotification) {
|
|
final PageMetrics metrics = notification.metrics;
|
|
final int currentPage = metrics.page.round();
|
|
if (currentPage != _lastReportedPage) {
|
|
_lastReportedPage = currentPage;
|
|
widget.onPageChanged(currentPage);
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
child: Scrollable(
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
axisDirection: axisDirection,
|
|
controller: widget.controller,
|
|
physics: physics,
|
|
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
assert(constraints.hasBoundedHeight);
|
|
assert(constraints.hasBoundedWidth);
|
|
|
|
double cacheExtent;
|
|
|
|
switch (widget.scrollDirection) {
|
|
case Axis.vertical:
|
|
cacheExtent = constraints.maxHeight * widget.extents;
|
|
break;
|
|
|
|
case Axis.horizontal:
|
|
default:
|
|
cacheExtent = constraints.maxWidth * widget.extents;
|
|
break;
|
|
}
|
|
|
|
return Viewport(
|
|
cacheExtent: cacheExtent,
|
|
axisDirection: axisDirection,
|
|
offset: position,
|
|
slivers: <Widget>[
|
|
SliverFillViewport(
|
|
viewportFraction: widget.controller.viewportFraction,
|
|
delegate: widget.childrenDelegate,
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
|
super.debugFillProperties(description);
|
|
description
|
|
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
|
|
description.add(
|
|
FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'),
|
|
);
|
|
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',
|
|
),
|
|
);
|
|
}
|
|
}
|