page_view.dart 30 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:math' as math;
7

8
import 'package:flutter/physics.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/gestures.dart' show DragStartBehavior;
11
import 'package:flutter/foundation.dart' show precisionErrorTolerance;
12 13

import 'basic.dart';
14
import 'debug.dart';
15 16
import 'framework.dart';
import 'notification_listener.dart';
17
import 'page_storage.dart';
18
import 'scroll_context.dart';
19
import 'scroll_controller.dart';
20
import 'scroll_metrics.dart';
21 22 23
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
24
import 'scroll_position_with_single_context.dart';
25
import 'scroll_view.dart';
26
import 'scrollable.dart';
27
import 'sliver.dart';
28
import 'sliver_fill.dart';
29
import 'viewport.dart';
30

31 32 33
/// A controller for [PageView].
///
/// A page controller lets you manipulate which page is visible in a [PageView].
Adam Barth's avatar
Adam Barth committed
34 35 36
/// 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.
37 38 39
///
/// See also:
///
40
///  * [PageView], which is the widget this object controls.
41
///
42
/// {@tool snippet}
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
///
/// This widget introduces a [MaterialApp], [Scaffold] and [PageView] with two pages
/// using the default constructor. Both pages contain a [RaisedButton] allowing you
/// to animate the [PageView] using a [PageController].
///
/// ```dart
/// class MyPageView extends StatefulWidget {
///   MyPageView({Key key}) : super(key: key);
///
///   _MyPageViewState createState() => _MyPageViewState();
/// }
///
/// class _MyPageViewState extends State<MyPageView> {
///   PageController _pageController;
///
///   @override
///   void initState() {
///     super.initState();
///     _pageController = PageController();
///   }
///
///   @override
///   void dispose() {
///     _pageController.dispose();
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return MaterialApp(
///       home: Scaffold(
///         body: PageView(
///           controller: _pageController,
///           children: [
///             Container(
///               color: Colors.red,
///               child: Center(
///                 child: RaisedButton(
///                   color: Colors.white,
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         1,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
///                   child: Text('Next'),
///                 ),
///               ),
///             ),
///             Container(
///               color: Colors.blue,
///               child: Center(
///                 child: RaisedButton(
///                   color: Colors.white,
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         0,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
///                   child: Text('Previous'),
///                 ),
///               ),
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
///
/// ```
/// {@end-tool}
122
class PageController extends ScrollController {
123 124
  /// Creates a page controller.
  ///
125
  /// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null.
126
  PageController({
127 128 129
    this.initialPage = 0,
    this.keepPage = true,
    this.viewportFraction = 1.0,
130
  }) : assert(initialPage != null),
131
       assert(keepPage != null),
132 133
       assert(viewportFraction != null),
       assert(viewportFraction > 0.0);
134

135
  /// The page to show when first creating the [PageView].
136
  final int initialPage;
137

138 139 140 141 142 143 144 145 146 147 148 149 150
  /// Save the current [page] with [PageStorage] and restore it if
  /// this controller's scrollable is recreated.
  ///
  /// If this property is set to false, the current [page] is never saved
  /// and [initialPage] is always used to initialize the scroll offset.
  /// If true (the default), the initial page is used the first time the
  /// controller's scrollable is created, since there's isn't a page to
  /// restore yet. Subsequently the saved page is restored and
  /// [initialPage] is ignored.
  ///
  /// See also:
  ///
  ///  * [PageStorageKey], which should be used when more than one
151
  ///    scrollable appears in the same route, to distinguish the [PageStorage]
152 153 154
  ///    locations used to save scroll offsets.
  final bool keepPage;

155 156 157 158 159 160 161
  /// The fraction of the viewport that each page should occupy.
  ///
  /// Defaults to 1.0, which means each page fills the viewport in the scrolling
  /// direction.
  final double viewportFraction;

  /// The current page displayed in the controlled [PageView].
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
  ///
  /// There are circumstances that this [PageController] can't know the current
  /// page. Reading [page] will throw an [AssertionError] in the following cases:
  ///
  /// 1. No [PageView] is currently using this [PageController]. Once a
  /// [PageView] starts using this [PageController], the new [page]
  /// position will be derived:
  ///
  ///   * First, based on the attached [PageView]'s [BuildContext] and the
  ///     position saved at that context's [PageStorage] if [keepPage] is true.
  ///   * Second, from the [PageController]'s [initialPage].
  ///
  /// 2. More than one [PageView] using the same [PageController].
  ///
  /// The [hasClients] property can be used to check if a [PageView] is attached
  /// prior to accessing [page].
178
  double get page {
179 180 181 182 183 184
    assert(
      positions.isNotEmpty,
      'PageController.page cannot be accessed before a PageView is built with it.',
    );
    assert(
      positions.length == 1,
185 186
      'The page property cannot be read when multiple PageViews are attached to '
      'the same PageController.',
187
    );
188
    final _PagePosition position = this.position as _PagePosition;
189
    return position.page;
190 191
  }

192 193 194 195 196 197
  /// Animates the controlled [PageView] from the current page to the given page.
  ///
  /// The animation lasts for the given duration and follows the given curve.
  /// The returned [Future] resolves when the animation completes.
  ///
  /// The `duration` and `curve` arguments must not be null.
198 199
  Future<void> animateToPage(
    int page, {
200 201 202
    @required Duration duration,
    @required Curve curve,
  }) {
203
    final _PagePosition position = this.position as _PagePosition;
204 205 206 207 208
    return position.animateTo(
      position.getPixelsFromPage(page.toDouble()),
      duration: duration,
      curve: curve,
    );
209 210
  }

211 212
  /// Changes which page is displayed in the controlled [PageView].
  ///
213 214
  /// Jumps the page position from its current value to the given value,
  /// without animation, and without checking if the new value is in range.
215
  void jumpToPage(int page) {
216
    final _PagePosition position = this.position as _PagePosition;
217
    position.jumpTo(position.getPixelsFromPage(page.toDouble()));
218 219
  }

220 221 222 223 224 225
  /// Animates the controlled [PageView] to the next page.
  ///
  /// The animation lasts for the given duration and follows the given curve.
  /// The returned [Future] resolves when the animation completes.
  ///
  /// The `duration` and `curve` arguments must not be null.
226
  Future<void> nextPage({ @required Duration duration, @required Curve curve }) {
227
    return animateToPage(page.round() + 1, duration: duration, curve: curve);
228 229
  }

230 231 232 233 234 235
  /// Animates the controlled [PageView] to the previous page.
  ///
  /// The animation lasts for the given duration and follows the given curve.
  /// The returned [Future] resolves when the animation completes.
  ///
  /// The `duration` and `curve` arguments must not be null.
236
  Future<void> previousPage({ @required Duration duration, @required Curve curve }) {
237
    return animateToPage(page.round() - 1, duration: duration, curve: curve);
238 239 240
  }

  @override
241
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
242
    return _PagePosition(
243
      physics: physics,
244
      context: context,
245
      initialPage: initialPage,
246
      keepPage: keepPage,
247
      viewportFraction: viewportFraction,
248 249 250
      oldPosition: oldPosition,
    );
  }
251 252 253 254

  @override
  void attach(ScrollPosition position) {
    super.attach(position);
255
    final _PagePosition pagePosition = position as _PagePosition;
256 257 258 259 260 261 262 263
    pagePosition.viewportFraction = viewportFraction;
  }
}

/// Metrics for a [PageView].
///
/// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s.
264
class PageMetrics extends FixedScrollMetrics {
265
  /// Creates an immutable snapshot of values associated with a [PageView].
266
  PageMetrics({
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
    @required double minScrollExtent,
    @required double maxScrollExtent,
    @required double pixels,
    @required double viewportDimension,
    @required AxisDirection axisDirection,
    @required this.viewportFraction,
  }) : super(
         minScrollExtent: minScrollExtent,
         maxScrollExtent: maxScrollExtent,
         pixels: pixels,
         viewportDimension: viewportDimension,
         axisDirection: axisDirection,
       );

  @override
  PageMetrics copyWith({
    double minScrollExtent,
    double maxScrollExtent,
    double pixels,
    double viewportDimension,
    AxisDirection axisDirection,
    double viewportFraction,
  }) {
290
    return PageMetrics(
291 292 293 294 295 296 297 298
      minScrollExtent: minScrollExtent ?? this.minScrollExtent,
      maxScrollExtent: maxScrollExtent ?? this.maxScrollExtent,
      pixels: pixels ?? this.pixels,
      viewportDimension: viewportDimension ?? this.viewportDimension,
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
    );
  }
299 300

  /// The current page displayed in the [PageView].
301 302 303 304 305 306 307 308 309
  double get page {
    return math.max(0.0, pixels.clamp(minScrollExtent, maxScrollExtent)) /
           math.max(1.0, viewportDimension * viewportFraction);
  }

  /// The fraction of the viewport that each page occupies.
  ///
  /// Used to compute [page] from the current [pixels].
  final double viewportFraction;
310 311
}

312
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
313 314
  _PagePosition({
    ScrollPhysics physics,
315
    ScrollContext context,
316 317 318
    this.initialPage = 0,
    bool keepPage = true,
    double viewportFraction = 1.0,
319
    ScrollPosition oldPosition,
320
  }) : assert(initialPage != null),
321
       assert(keepPage != null),
322 323 324
       assert(viewportFraction != null),
       assert(viewportFraction > 0.0),
       _viewportFraction = viewportFraction,
325 326
       _pageToUseOnStartup = initialPage.toDouble(),
       super(
327 328 329
         physics: physics,
         context: context,
         initialPixels: null,
330
         keepScrollOffset: keepPage,
331 332
         oldPosition: oldPosition,
       );
333

334
  final int initialPage;
335
  double _pageToUseOnStartup;
336

337
  @override
338 339
  double get viewportFraction => _viewportFraction;
  double _viewportFraction;
340 341
  set viewportFraction(double value) {
    if (_viewportFraction == value)
342 343
      return;
    final double oldPage = page;
344
    _viewportFraction = value;
345
    if (oldPage != null)
346
      forcePixels(getPixelsFromPage(oldPage));
347 348
  }

349 350 351 352 353 354 355 356
  // The amount of offset that will be added to [minScrollExtent] and subtracted
  // from [maxScrollExtent], such that every page will properly snap to the center
  // of the viewport when viewportFraction is greater than 1.
  //
  // The value is 0 if viewportFraction is less than or equal to 1, larger than 0
  // otherwise.
  double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);

357
  double getPageFromPixels(double pixels, double viewportDimension) {
358
    final double actual = math.max(0.0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction);
359 360 361 362 363
    final double round = actual.roundToDouble();
    if ((actual - round).abs() < precisionErrorTolerance) {
      return round;
    }
    return actual;
364 365 366
  }

  double getPixelsFromPage(double page) {
367
    return page * viewportDimension * viewportFraction + _initialPageOffset;
368 369
  }

370
  @override
371 372 373 374 375
  double get page {
    assert(
      pixels == null || (minScrollExtent != null && maxScrollExtent != null),
      'Page value is only available after content dimensions are established.',
    );
376
    return pixels == null ? null : getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent) as double, viewportDimension);
377
  }
378

379 380 381 382 383 384 385 386
  @override
  void saveScrollOffset() {
    PageStorage.of(context.storageContext)?.writeState(context.storageContext, getPageFromPixels(pixels, viewportDimension));
  }

  @override
  void restoreScrollOffset() {
    if (pixels == null) {
387
      final double value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double;
388
      if (value != null)
389
        _pageToUseOnStartup = value;
390 391 392
    }
  }

393 394 395 396 397
  @override
  bool applyViewportDimension(double viewportDimension) {
    final double oldViewportDimensions = this.viewportDimension;
    final bool result = super.applyViewportDimension(viewportDimension);
    final double oldPixels = pixels;
398
    final double page = (oldPixels == null || oldViewportDimensions == 0.0) ? _pageToUseOnStartup : getPageFromPixels(oldPixels, oldViewportDimensions);
399
    final double newPixels = getPixelsFromPage(page);
400

401 402 403 404 405 406
    if (newPixels != oldPixels) {
      correctPixels(newPixels);
      return false;
    }
    return result;
  }
407

408 409 410 411 412 413 414 415 416
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
    return super.applyContentDimensions(
      newMinScrollExtent,
      math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
    );
  }

417
  @override
418 419 420 421 422 423 424 425
  PageMetrics copyWith({
    double minScrollExtent,
    double maxScrollExtent,
    double pixels,
    double viewportDimension,
    AxisDirection axisDirection,
    double viewportFraction,
  }) {
426
    return PageMetrics(
427 428 429 430 431 432
      minScrollExtent: minScrollExtent ?? this.minScrollExtent,
      maxScrollExtent: maxScrollExtent ?? this.maxScrollExtent,
      pixels: pixels ?? this.pixels,
      viewportDimension: viewportDimension ?? this.viewportDimension,
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
433 434 435 436
    );
  }
}

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
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;
}

456 457 458
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
459 460 461 462 463 464
///
/// See also:
///
///  * [ScrollPhysics], the base class which defines the API for scrolling
///    physics.
///  * [PageView.physics], which can override the physics used by a page view.
465 466
class PageScrollPhysics extends ScrollPhysics {
  /// Creates physics for a [PageView].
467
  const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
468 469

  @override
470
  PageScrollPhysics applyTo(ScrollPhysics ancestor) {
471
    return PageScrollPhysics(parent: buildParent(ancestor));
472
  }
473

474
  double _getPage(ScrollMetrics position) {
475 476 477 478 479
    if (position is _PagePosition)
      return position.page;
    return position.pixels / position.viewportDimension;
  }

480
  double _getPixels(ScrollMetrics position, double page) {
481 482 483 484 485
    if (position is _PagePosition)
      return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

486
  double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
487 488 489 490 491 492 493 494 495
    double page = _getPage(position);
    if (velocity < -tolerance.velocity)
      page -= 0.5;
    else if (velocity > tolerance.velocity)
      page += 0.5;
    return _getPixels(position, page.roundToDouble());
  }

  @override
496
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
497 498 499 500 501 502
    // If we're out of range and not headed back in range, defer to the parent
    // ballistics, which should put us back in range at a page boundary.
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
      return super.createBallisticSimulation(position, velocity);
    final Tolerance tolerance = this.tolerance;
503
    final double target = _getTargetPixels(position, tolerance, velocity);
504
    if (target != position.pixels)
505
      return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
506
    return null;
507
  }
508 509 510

  @override
  bool get allowImplicitScrolling => false;
511 512 513 514 515 516
}

// 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.
517
final PageController _defaultPageController = PageController();
518
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
519 520

/// A scrollable list that works page by page.
Adam Barth's avatar
Adam Barth committed
521 522 523 524 525 526 527 528 529 530 531 532
///
/// 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.
533
///
534 535
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
536 537
/// See also:
///
Adam Barth's avatar
Adam Barth committed
538 539 540 541
///  * [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.
542
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
543
///    the scroll position without using a [ScrollController].
544
class PageView extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
545 546 547 548 549 550 551
  /// 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.
552 553 554 555 556 557 558
  ///
  /// {@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}
559 560
  PageView({
    Key key,
561 562
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
563
    PageController controller,
564
    this.physics,
565
    this.pageSnapping = true,
566
    this.onPageChanged,
567
    List<Widget> children = const <Widget>[],
568
    this.dragStartBehavior = DragStartBehavior.start,
569 570 571
    this.allowImplicitScrolling = false,
  }) : assert(allowImplicitScrolling != null),
       controller = controller ?? _defaultPageController,
572
       childrenDelegate = SliverChildListDelegate(children),
573
       super(key: key);
574

Adam Barth's avatar
Adam Barth committed
575 576 577 578 579 580 581 582 583 584 585 586
  /// 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].
587 588 589 590
  ///
  /// [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].
591 592
  ///
  /// {@macro flutter.widgets.pageView.allowImplicitScrolling}
593 594
  PageView.builder({
    Key key,
595 596
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
597
    PageController controller,
598
    this.physics,
599
    this.pageSnapping = true,
600
    this.onPageChanged,
601
    @required IndexedWidgetBuilder itemBuilder,
602
    int itemCount,
603
    this.dragStartBehavior = DragStartBehavior.start,
604 605 606
    this.allowImplicitScrolling = false,
  }) : assert(allowImplicitScrolling != null),
       controller = controller ?? _defaultPageController,
607
       childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
608
       super(key: key);
609

Adam Barth's avatar
Adam Barth committed
610 611
  /// Creates a scrollable list that works page by page with a custom child
  /// model.
612
  ///
613
  /// {@tool snippet}
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
  ///
  /// 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}
690 691
  ///
  /// {@macro flutter.widgets.pageView.allowImplicitScrolling}
692 693
  PageView.custom({
    Key key,
694 695
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
696
    PageController controller,
697
    this.physics,
698
    this.pageSnapping = true,
699 700
    this.onPageChanged,
    @required this.childrenDelegate,
701
    this.dragStartBehavior = DragStartBehavior.start,
702
    this.allowImplicitScrolling = false,
703
  }) : assert(childrenDelegate != null),
704
       assert(allowImplicitScrolling != null),
705 706
       controller = controller ?? _defaultPageController,
       super(key: key);
707

708 709 710 711 712 713 714 715 716 717 718 719 720
  /// 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;

Adam Barth's avatar
Adam Barth committed
721 722 723
  /// The axis along which the page view scrolls.
  ///
  /// Defaults to [Axis.horizontal].
724 725
  final Axis scrollDirection;

Adam Barth's avatar
Adam Barth committed
726 727 728 729 730 731 732 733 734 735 736 737
  /// 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.
738 739
  final bool reverse;

Adam Barth's avatar
Adam Barth committed
740 741
  /// An object that can be used to control the position to which this page
  /// view is scrolled.
742 743
  final PageController controller;

Adam Barth's avatar
Adam Barth committed
744 745 746 747 748 749 750 751 752
  /// 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.
753 754
  final ScrollPhysics physics;

755 756 757
  /// Set to false to disable page snapping, useful for custom scroll behavior.
  final bool pageSnapping;

Adam Barth's avatar
Adam Barth committed
758
  /// Called whenever the page in the center of the viewport changes.
759 760
  final ValueChanged<int> onPageChanged;

Adam Barth's avatar
Adam Barth committed
761 762 763 764 765 766
  /// 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.
767 768
  final SliverChildDelegate childrenDelegate;

769 770 771
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

772
  @override
773
  _PageViewState createState() => _PageViewState();
774 775 776 777 778 779 780 781
}

class _PageViewState extends State<PageView> {
  int _lastReportedPage = 0;

  @override
  void initState() {
    super.initState();
782
    _lastReportedPage = widget.controller.initialPage;
783 784 785
  }

  AxisDirection _getDirection(BuildContext context) {
786
    switch (widget.scrollDirection) {
787
      case Axis.horizontal:
788
        assert(debugCheckHasDirectionality(context));
789
        final TextDirection textDirection = Directionality.of(context);
790 791
        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
        return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
792
      case Axis.vertical:
793
        return widget.reverse ? AxisDirection.up : AxisDirection.down;
794 795
    }
    return null;
796 797 798 799
  }

  @override
  Widget build(BuildContext context) {
800
    final AxisDirection axisDirection = _getDirection(context);
801 802 803
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
    ).applyTo(widget.pageSnapping
804
        ? _kPagePhysics.applyTo(widget.physics)
805
        : widget.physics);
806

807
    return NotificationListener<ScrollNotification>(
Adam Barth's avatar
Adam Barth committed
808
      onNotification: (ScrollNotification notification) {
809
        if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
810
          final PageMetrics metrics = notification.metrics as PageMetrics;
811
          final int currentPage = metrics.page.round();
812 813
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
814
            widget.onPageChanged(currentPage);
815
          }
816
        }
817
        return false;
818
      },
819
      child: Scrollable(
820
        dragStartBehavior: widget.dragStartBehavior,
821
        axisDirection: axisDirection,
822
        controller: widget.controller,
823
        physics: physics,
824
        viewportBuilder: (BuildContext context, ViewportOffset position) {
825
          return Viewport(
826 827 828 829 830
            // 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,
831
            axisDirection: axisDirection,
832
            offset: position,
833
            slivers: <Widget>[
834
              SliverFillViewport(
835
                viewportFraction: widget.controller.viewportFraction,
836
                delegate: widget.childrenDelegate,
837
              ),
838 839 840 841
            ],
          );
        },
      ),
842 843
    );
  }
844 845

  @override
846
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
847
    super.debugFillProperties(description);
848 849 850 851 852
    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'));
853
    description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
854
  }
855
}