|
@@ -0,0 +1,381 @@
|
|
|
+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 = 0,
|
|
|
+ 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'));
|
|
|
+ }
|
|
|
+}
|