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

5
import 'dart:math' as math;
6

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

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

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

126
  /// The page to show when first creating the [PageView].
127
  final int initialPage;
128

129 130 131 132 133 134 135 136 137 138 139 140 141
  /// 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
142
  ///    scrollable appears in the same route, to distinguish the [PageStorage]
143 144 145
  ///    locations used to save scroll offsets.
  final bool keepPage;

146 147 148 149 150 151 152
  /// 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].
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  ///
  /// 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].
169
  double? get page {
170 171 172 173 174 175
    assert(
      positions.isNotEmpty,
      'PageController.page cannot be accessed before a PageView is built with it.',
    );
    assert(
      positions.length == 1,
176 177
      'The page property cannot be read when multiple PageViews are attached to '
      'the same PageController.',
178
    );
179
    final _PagePosition position = this.position as _PagePosition;
180
    return position.page;
181 182
  }

183 184 185 186 187 188
  /// 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.
189 190
  Future<void> animateToPage(
    int page, {
191 192
    required Duration duration,
    required Curve curve,
193
  }) {
194
    final _PagePosition position = this.position as _PagePosition;
195 196 197 198 199
    return position.animateTo(
      position.getPixelsFromPage(page.toDouble()),
      duration: duration,
      curve: curve,
    );
200 201
  }

202 203
  /// Changes which page is displayed in the controlled [PageView].
  ///
204 205
  /// Jumps the page position from its current value to the given value,
  /// without animation, and without checking if the new value is in range.
206
  void jumpToPage(int page) {
207
    final _PagePosition position = this.position as _PagePosition;
208
    position.jumpTo(position.getPixelsFromPage(page.toDouble()));
209 210
  }

211 212 213 214 215 216
  /// 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.
217 218
  Future<void> nextPage({ required Duration duration, required Curve curve }) {
    return animateToPage(page!.round() + 1, duration: duration, curve: curve);
219 220
  }

221 222 223 224 225 226
  /// 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.
227 228
  Future<void> previousPage({ required Duration duration, required Curve curve }) {
    return animateToPage(page!.round() - 1, duration: duration, curve: curve);
229 230 231
  }

  @override
232
  ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
233
    return _PagePosition(
234
      physics: physics,
235
      context: context,
236
      initialPage: initialPage,
237
      keepPage: keepPage,
238
      viewportFraction: viewportFraction,
239 240 241
      oldPosition: oldPosition,
    );
  }
242 243 244 245

  @override
  void attach(ScrollPosition position) {
    super.attach(position);
246
    final _PagePosition pagePosition = position as _PagePosition;
247 248 249 250 251 252 253 254
    pagePosition.viewportFraction = viewportFraction;
  }
}

/// Metrics for a [PageView].
///
/// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s.
255
class PageMetrics extends FixedScrollMetrics {
256
  /// Creates an immutable snapshot of values associated with a [PageView].
257
  PageMetrics({
258 259 260 261
    required double? minScrollExtent,
    required double? maxScrollExtent,
    required double? pixels,
    required double? viewportDimension,
262 263
    required AxisDirection axisDirection,
    required this.viewportFraction,
264 265 266 267 268 269 270 271 272 273
  }) : super(
         minScrollExtent: minScrollExtent,
         maxScrollExtent: maxScrollExtent,
         pixels: pixels,
         viewportDimension: viewportDimension,
         axisDirection: axisDirection,
       );

  @override
  PageMetrics copyWith({
274 275 276 277 278 279
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? viewportFraction,
280
  }) {
281
    return PageMetrics(
282 283 284 285
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
286 287 288 289
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
    );
  }
290 291

  /// The current page displayed in the [PageView].
292
  double? get page {
293 294 295 296 297 298 299 300
    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;
301 302
}

303
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
304
  _PagePosition({
305 306
    required ScrollPhysics physics,
    required ScrollContext context,
307 308 309
    this.initialPage = 0,
    bool keepPage = true,
    double viewportFraction = 1.0,
310
    ScrollPosition? oldPosition,
311
  }) : assert(initialPage != null),
312
       assert(keepPage != null),
313 314 315
       assert(viewportFraction != null),
       assert(viewportFraction > 0.0),
       _viewportFraction = viewportFraction,
316 317
       _pageToUseOnStartup = initialPage.toDouble(),
       super(
318 319 320
         physics: physics,
         context: context,
         initialPixels: null,
321
         keepScrollOffset: keepPage,
322 323
         oldPosition: oldPosition,
       );
324

325
  final int initialPage;
326
  double _pageToUseOnStartup;
327

328 329 330 331 332 333 334 335 336 337
  @override
  Future<void> ensureVisible(
    RenderObject object, {
    double alignment = 0.0,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
    ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
    RenderObject? targetRenderObject,
  }) {
    // Since the _PagePosition is intended to cover the available space within
338 339 340 341 342 343 344 345 346 347 348
    // its viewport, stop trying to move the target render object to the center
    // - otherwise, could end up changing which page is visible and moving the
    // targetRenderObject out of the viewport.
    return super.ensureVisible(
      object,
      alignment: alignment,
      duration: duration,
      curve: curve,
      alignmentPolicy: alignmentPolicy,
      targetRenderObject: null,
    );
349 350
  }

351
  @override
352 353
  double get viewportFraction => _viewportFraction;
  double _viewportFraction;
354 355
  set viewportFraction(double value) {
    if (_viewportFraction == value)
356
      return;
357
    final double? oldPage = page;
358
    _viewportFraction = value;
359
    if (oldPage != null)
360
      forcePixels(getPixelsFromPage(oldPage));
361 362
  }

363 364 365 366 367 368 369 370
  // 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);

371
  double getPageFromPixels(double pixels, double viewportDimension) {
372
    final double actual = math.max(0.0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction);
373 374 375 376 377
    final double round = actual.roundToDouble();
    if ((actual - round).abs() < precisionErrorTolerance) {
      return round;
    }
    return actual;
378 379 380
  }

  double getPixelsFromPage(double page) {
381
    return page * viewportDimension * viewportFraction + _initialPageOffset;
382 383
  }

384
  @override
385
  double? get page {
386
    assert(
387
      !hasPixels || (minScrollExtent != null && maxScrollExtent != null),
388 389
      'Page value is only available after content dimensions are established.',
    );
390
    return !hasPixels ? null : getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
391
  }
392

393 394 395 396 397 398 399
  @override
  void saveScrollOffset() {
    PageStorage.of(context.storageContext)?.writeState(context.storageContext, getPageFromPixels(pixels, viewportDimension));
  }

  @override
  void restoreScrollOffset() {
400 401
    if (!hasPixels) {
      final double? value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double?;
402
      if (value != null)
403
        _pageToUseOnStartup = value;
404 405 406
    }
  }

407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
  @override
  void saveOffset() {
    context.saveOffset(getPageFromPixels(pixels, viewportDimension));
  }

  @override
  void restoreOffset(double offset, {bool initialRestore = false}) {
    assert(initialRestore != null);
    assert(offset != null);
    if (initialRestore) {
      _pageToUseOnStartup = offset;
    } else {
      jumpTo(getPixelsFromPage(offset));
    }
  }

423 424
  @override
  bool applyViewportDimension(double viewportDimension) {
425
    final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
426 427 428
    if (viewportDimension == oldViewportDimensions) {
      return true;
    }
429
    final bool result = super.applyViewportDimension(viewportDimension);
430 431
    final double? oldPixels = hasPixels ? pixels : null;
    final double page = (oldPixels == null || oldViewportDimensions == 0.0) ? _pageToUseOnStartup : getPageFromPixels(oldPixels, oldViewportDimensions!);
432
    final double newPixels = getPixelsFromPage(page);
433

434 435 436 437 438 439
    if (newPixels != oldPixels) {
      correctPixels(newPixels);
      return false;
    }
    return result;
  }
440

441 442 443 444 445 446 447 448 449
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
    return super.applyContentDimensions(
      newMinScrollExtent,
      math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
    );
  }

450
  @override
451
  PageMetrics copyWith({
452 453 454 455 456 457
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? viewportFraction,
458
  }) {
459
    return PageMetrics(
460 461 462 463
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
464 465
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
466 467 468 469
    );
  }
}

470 471
class _ForceImplicitScrollPhysics extends ScrollPhysics {
  const _ForceImplicitScrollPhysics({
472 473
    required this.allowImplicitScrolling,
    ScrollPhysics? parent,
474 475 476 477
  }) : assert(allowImplicitScrolling != null),
       super(parent: parent);

  @override
478
  _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
479 480 481 482 483 484 485 486 487 488
    return _ForceImplicitScrollPhysics(
      allowImplicitScrolling: allowImplicitScrolling,
      parent: buildParent(ancestor),
    );
  }

  @override
  final bool allowImplicitScrolling;
}

489 490 491
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
492 493 494 495 496 497
///
/// 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.
498 499
class PageScrollPhysics extends ScrollPhysics {
  /// Creates physics for a [PageView].
500
  const PageScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);
501 502

  @override
503
  PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
504
    return PageScrollPhysics(parent: buildParent(ancestor));
505
  }
506

507
  double _getPage(ScrollMetrics position) {
508
    if (position is _PagePosition)
509
      return position.page!;
510 511 512
    return position.pixels / position.viewportDimension;
  }

513
  double _getPixels(ScrollMetrics position, double page) {
514 515 516 517 518
    if (position is _PagePosition)
      return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

519
  double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
520 521 522 523 524 525 526 527 528
    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
529
  Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
530 531 532 533 534 535
    // 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;
536
    final double target = _getTargetPixels(position, tolerance, velocity);
537
    if (target != position.pixels)
538
      return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
539
    return null;
540
  }
541 542 543

  @override
  bool get allowImplicitScrolling => false;
544 545 546 547 548 549
}

// 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.
550
final PageController _defaultPageController = PageController();
551
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
552 553

/// A scrollable list that works page by page.
Adam Barth's avatar
Adam Barth committed
554 555 556 557 558 559 560 561 562 563 564 565
///
/// 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.
566
///
567 568
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
569 570 571 572 573 574 575
/// {@tool dartpad --template=stateless_widget_scaffold}
///
/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally.
///
/// ```dart
///  Widget build(BuildContext context) {
576
///    final PageController controller = PageController(initialPage: 0);
577 578 579 580 581
///    return PageView(
///      /// [PageView.scrollDirection] defaults to [Axis.horizontal].
///      /// Use [Axis.vertical] to scroll vertically.
///      scrollDirection: Axis.horizontal,
///      controller: controller,
582
///      children: const <Widget>[
583
///        Center(
584
///          child: Text('First Page'),
585 586
///        ),
///        Center(
587
///          child: Text('Second Page'),
588 589
///        ),
///        Center(
590
///          child: Text('Third Page'),
591 592 593 594 595 596 597
///        )
///      ],
///    );
///  }
/// ```
/// {@end-tool}
///
598 599
/// See also:
///
Adam Barth's avatar
Adam Barth committed
600 601 602 603
///  * [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.
604
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
605
///    the scroll position without using a [ScrollController].
606
class PageView extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
607 608 609 610 611 612 613
  /// 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.
614
  ///
615 616 617 618
  /// Like other widgets in the framework, this widget expects that
  /// the [children] list will not be mutated after it has been passed in here.
  /// See the documentation at [SliverChildListDelegate.children] for more details.
  ///
619
  /// {@template flutter.widgets.PageView.allowImplicitScrolling}
620 621 622 623 624
  /// 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}
625
  PageView({
626
    Key? key,
627 628
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
629
    PageController? controller,
630
    this.physics,
631
    this.pageSnapping = true,
632
    this.onPageChanged,
633
    List<Widget> children = const <Widget>[],
634
    this.dragStartBehavior = DragStartBehavior.start,
635
    this.allowImplicitScrolling = false,
636
    this.restorationId,
637
    this.clipBehavior = Clip.hardEdge,
638
  }) : assert(allowImplicitScrolling != null),
639
       assert(clipBehavior != null),
640
       controller = controller ?? _defaultPageController,
641
       childrenDelegate = SliverChildListDelegate(children),
642
       super(key: key);
643

Adam Barth's avatar
Adam Barth committed
644 645 646 647 648 649 650 651 652 653 654 655
  /// 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].
656 657 658 659
  ///
  /// [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].
660
  ///
661
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
662
  PageView.builder({
663
    Key? key,
664 665
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
666
    PageController? controller,
667
    this.physics,
668
    this.pageSnapping = true,
669
    this.onPageChanged,
670 671
    required IndexedWidgetBuilder itemBuilder,
    int? itemCount,
672
    this.dragStartBehavior = DragStartBehavior.start,
673
    this.allowImplicitScrolling = false,
674
    this.restorationId,
675
    this.clipBehavior = Clip.hardEdge,
676
  }) : assert(allowImplicitScrolling != null),
677
       assert(clipBehavior != null),
678
       controller = controller ?? _defaultPageController,
679
       childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
680
       super(key: key);
681

Adam Barth's avatar
Adam Barth committed
682 683
  /// Creates a scrollable list that works page by page with a custom child
  /// model.
684
  ///
685
  /// {@tool snippet}
686 687 688 689 690 691
  ///
  /// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child
  /// reordering.
  ///
  /// ```dart
  /// class MyPageView extends StatefulWidget {
692 693
  ///   const MyPageView({Key? key}) : super(key: key);
  ///
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
  ///   @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) {
721
  ///               final ValueKey<String> valueKey = key as ValueKey<String>;
722 723 724 725 726 727 728 729 730 731
  ///               final String data = valueKey.value;
  ///               return items.indexOf(data);
  ///             }
  ///           ),
  ///         ),
  ///       ),
  ///       bottomNavigationBar: BottomAppBar(
  ///         child: Row(
  ///           mainAxisAlignment: MainAxisAlignment.center,
  ///           children: <Widget>[
732
  ///             TextButton(
733
  ///               onPressed: () => _reverse(),
734
  ///               child: const Text('Reverse items'),
735 736 737 738 739 740 741 742 743
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  ///
  /// class KeepAlive extends StatefulWidget {
744
  ///   const KeepAlive({Key? key, required this.data}) : super(key: key);
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
  ///
  ///   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}
764
  ///
765
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
766
  PageView.custom({
767
    Key? key,
768 769
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
770
    PageController? controller,
771
    this.physics,
772
    this.pageSnapping = true,
773
    this.onPageChanged,
774
    required this.childrenDelegate,
775
    this.dragStartBehavior = DragStartBehavior.start,
776
    this.allowImplicitScrolling = false,
777
    this.restorationId,
778
    this.clipBehavior = Clip.hardEdge,
779
  }) : assert(childrenDelegate != null),
780
       assert(allowImplicitScrolling != null),
781
       assert(clipBehavior != null),
782 783
       controller = controller ?? _defaultPageController,
       super(key: key);
784

785 786 787 788 789 790 791 792 793 794 795 796 797
  /// 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;

798
  /// {@macro flutter.widgets.scrollable.restorationId}
799
  final String? restorationId;
800

Adam Barth's avatar
Adam Barth committed
801 802 803
  /// The axis along which the page view scrolls.
  ///
  /// Defaults to [Axis.horizontal].
804 805
  final Axis scrollDirection;

Adam Barth's avatar
Adam Barth committed
806 807 808 809 810 811 812 813 814 815 816 817
  /// 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.
818 819
  final bool reverse;

Adam Barth's avatar
Adam Barth committed
820 821
  /// An object that can be used to control the position to which this page
  /// view is scrolled.
822 823
  final PageController controller;

Adam Barth's avatar
Adam Barth committed
824 825 826 827 828 829 830 831 832
  /// 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.
833
  final ScrollPhysics? physics;
834

835 836 837
  /// Set to false to disable page snapping, useful for custom scroll behavior.
  final bool pageSnapping;

Adam Barth's avatar
Adam Barth committed
838
  /// Called whenever the page in the center of the viewport changes.
839
  final ValueChanged<int>? onPageChanged;
840

Adam Barth's avatar
Adam Barth committed
841 842 843 844 845 846
  /// 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.
847 848
  final SliverChildDelegate childrenDelegate;

849 850 851
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

852
  /// {@macro flutter.material.Material.clipBehavior}
853 854 855 856
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

857
  @override
858
  _PageViewState createState() => _PageViewState();
859 860 861 862 863 864 865 866
}

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

  @override
  void initState() {
    super.initState();
867
    _lastReportedPage = widget.controller.initialPage;
868 869 870
  }

  AxisDirection _getDirection(BuildContext context) {
871
    switch (widget.scrollDirection) {
872
      case Axis.horizontal:
873
        assert(debugCheckHasDirectionality(context));
874
        final TextDirection textDirection = Directionality.of(context);
875 876
        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
        return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
877
      case Axis.vertical:
878
        return widget.reverse ? AxisDirection.up : AxisDirection.down;
879
    }
880 881 882 883
  }

  @override
  Widget build(BuildContext context) {
884
    final AxisDirection axisDirection = _getDirection(context);
885 886 887
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
    ).applyTo(widget.pageSnapping
888
        ? _kPagePhysics.applyTo(widget.physics)
889
        : widget.physics);
890

891
    return NotificationListener<ScrollNotification>(
Adam Barth's avatar
Adam Barth committed
892
      onNotification: (ScrollNotification notification) {
893
        if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
894
          final PageMetrics metrics = notification.metrics as PageMetrics;
895
          final int currentPage = metrics.page!.round();
896 897
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
898
            widget.onPageChanged!(currentPage);
899
          }
900
        }
901
        return false;
902
      },
903
      child: Scrollable(
904
        dragStartBehavior: widget.dragStartBehavior,
905
        axisDirection: axisDirection,
906
        controller: widget.controller,
907
        physics: physics,
908
        restorationId: widget.restorationId,
909
        viewportBuilder: (BuildContext context, ViewportOffset position) {
910
          return Viewport(
911 912 913 914 915
            // 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,
916
            axisDirection: axisDirection,
917
            offset: position,
918
            clipBehavior: widget.clipBehavior,
919
            slivers: <Widget>[
920
              SliverFillViewport(
921
                viewportFraction: widget.controller.viewportFraction,
922
                delegate: widget.childrenDelegate,
923
              ),
924 925 926 927
            ],
          );
        },
      ),
928 929
    );
  }
930 931

  @override
932
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
933
    super.debugFillProperties(description);
934 935 936 937 938
    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'));
939
    description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
940
  }
941
}