page_view.dart 35.7 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/foundation.dart' show clampDouble, precisionErrorTolerance;
8 9
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
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_configuration.dart';
17
import 'scroll_context.dart';
18
import 'scroll_controller.dart';
19
import 'scroll_metrics.dart';
20 21 22
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
23
import 'scroll_position_with_single_context.dart';
24
import 'scroll_view.dart';
25
import 'scrollable.dart';
26
import 'sliver.dart';
27
import 'sliver_fill.dart';
28
import 'viewport.dart';
29

30 31 32
/// A controller for [PageView].
///
/// A page controller lets you manipulate which page is visible in a [PageView].
Adam Barth's avatar
Adam Barth committed
33 34 35
/// 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.
36 37 38
///
/// See also:
///
39
///  * [PageView], which is the widget this object controls.
40
///
41
/// {@tool snippet}
42 43
///
/// This widget introduces a [MaterialApp], [Scaffold] and [PageView] with two pages
44
/// using the default constructor. Both pages contain an [ElevatedButton] allowing you
45 46 47 48
/// to animate the [PageView] using a [PageController].
///
/// ```dart
/// class MyPageView extends StatefulWidget {
49
///   const MyPageView({super.key});
50
///
51
///   @override
52
///   State<MyPageView> createState() => _MyPageViewState();
53 54 55
/// }
///
/// class _MyPageViewState extends State<MyPageView> {
56
///   final PageController _pageController = PageController();
57 58 59 60 61 62 63 64 65 66 67 68 69
///
///   @override
///   void dispose() {
///     _pageController.dispose();
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return MaterialApp(
///       home: Scaffold(
///         body: PageView(
///           controller: _pageController,
70
///           children: <Widget>[
71
///             ColoredBox(
72 73
///               color: Colors.red,
///               child: Center(
74
///                 child: ElevatedButton(
75 76 77 78 79 80 81 82 83
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         1,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
84
///                   child: const Text('Next'),
85 86 87
///                 ),
///               ),
///             ),
88
///             ColoredBox(
89 90
///               color: Colors.blue,
///               child: Center(
91
///                 child: ElevatedButton(
92 93 94 95 96 97 98 99 100
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         0,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
101
///                   child: const Text('Previous'),
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
  /// {@template flutter.widgets.pageview.viewportFraction}
147 148 149 150
  /// 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.
151
  /// {@endtemplate}
152 153 154
  final double viewportFraction;

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

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

202 203 204 205 206
    return position.animateTo(
      position.getPixelsFromPage(page.toDouble()),
      duration: duration,
      curve: curve,
    );
207 208
  }

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

220
    position.jumpTo(position.getPixelsFromPage(page.toDouble()));
221 222
  }

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

233 234 235 236 237 238
  /// 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.
239 240
  Future<void> previousPage({ required Duration duration, required Curve curve }) {
    return animateToPage(page!.round() - 1, duration: duration, curve: curve);
241 242 243
  }

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

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

/// Metrics for a [PageView].
///
/// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s.
267
class PageMetrics extends FixedScrollMetrics {
268
  /// Creates an immutable snapshot of values associated with a [PageView].
269
  PageMetrics({
270 271 272 273 274
    required super.minScrollExtent,
    required super.maxScrollExtent,
    required super.pixels,
    required super.viewportDimension,
    required super.axisDirection,
275
    required this.viewportFraction,
276
    required super.devicePixelRatio,
277
  });
278 279 280

  @override
  PageMetrics copyWith({
281 282 283 284 285 286
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? viewportFraction,
287
    double? devicePixelRatio,
288
  }) {
289
    return PageMetrics(
290 291 292 293
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
294 295
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
296
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
297 298
    );
  }
299 300

  /// The current page displayed in the [PageView].
301
  double? get page {
302
    return math.max(0.0, clampDouble(pixels, minScrollExtent, maxScrollExtent)) /
303 304 305 306 307 308 309
           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
  _PagePosition({
314 315
    required super.physics,
    required super.context,
316 317 318
    this.initialPage = 0,
    bool keepPage = true,
    double viewportFraction = 1.0,
319
    super.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
         initialPixels: null,
328
         keepScrollOffset: keepPage,
329
       );
330

331
  final int initialPage;
332
  double _pageToUseOnStartup;
333 334 335 336
  // When the viewport has a zero-size, the `page` can not
  // be retrieved by `getPageFromPixels`, so we need to cache the page
  // for use when resizing the viewport to non-zero next time.
  double? _cachedPage;
337

338 339 340 341 342 343 344 345 346 347
  @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
348 349 350 351 352 353 354 355 356 357
    // 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,
    );
358 359
  }

360
  @override
361 362
  double get viewportFraction => _viewportFraction;
  double _viewportFraction;
363
  set viewportFraction(double value) {
364
    if (_viewportFraction == value) {
365
      return;
366
    }
367
    final double? oldPage = page;
368
    _viewportFraction = value;
369
    if (oldPage != null) {
370
      forcePixels(getPixelsFromPage(oldPage));
371
    }
372 373
  }

374 375 376 377 378 379 380 381
  // 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);

382
  double getPageFromPixels(double pixels, double viewportDimension) {
383 384
    assert(viewportDimension > 0.0);
    final double actual = math.max(0.0, pixels - _initialPageOffset) / (viewportDimension * viewportFraction);
385 386 387 388 389
    final double round = actual.roundToDouble();
    if ((actual - round).abs() < precisionErrorTolerance) {
      return round;
    }
    return actual;
390 391 392
  }

  double getPixelsFromPage(double page) {
393
    return page * viewportDimension * viewportFraction + _initialPageOffset;
394 395
  }

396
  @override
397
  double? get page {
398
    assert(
399
      !hasPixels || hasContentDimensions,
400 401
      'Page value is only available after content dimensions are established.',
    );
402 403
    return !hasPixels || !hasContentDimensions
      ? null
404
      : _cachedPage ?? getPageFromPixels(clampDouble(pixels, minScrollExtent, maxScrollExtent), viewportDimension);
405
  }
406

407 408
  @override
  void saveScrollOffset() {
409
    PageStorage.maybeOf(context.storageContext)?.writeState(context.storageContext, _cachedPage ?? getPageFromPixels(pixels, viewportDimension));
410 411 412 413
  }

  @override
  void restoreScrollOffset() {
414
    if (!hasPixels) {
415
      final double? value = PageStorage.maybeOf(context.storageContext)?.readState(context.storageContext) as double?;
416
      if (value != null) {
417
        _pageToUseOnStartup = value;
418
      }
419 420 421
    }
  }

422 423
  @override
  void saveOffset() {
424
    context.saveOffset(_cachedPage ?? getPageFromPixels(pixels, viewportDimension));
425 426 427 428 429 430 431 432 433 434 435 436 437
  }

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

438 439
  @override
  bool applyViewportDimension(double viewportDimension) {
440
    final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
441 442 443
    if (viewportDimension == oldViewportDimensions) {
      return true;
    }
444
    final bool result = super.applyViewportDimension(viewportDimension);
445
    final double? oldPixels = hasPixels ? pixels : null;
446 447 448 449 450 451 452 453 454
    double page;
    if (oldPixels == null) {
      page = _pageToUseOnStartup;
    } else if (oldViewportDimensions == 0.0) {
      // If resize from zero, we should use the _cachedPage to recover the state.
      page = _cachedPage!;
    } else {
      page = getPageFromPixels(oldPixels, oldViewportDimensions!);
    }
455
    final double newPixels = getPixelsFromPage(page);
456

457 458 459 460
    // If the viewportDimension is zero, cache the page
    // in case the viewport is resized to be non-zero.
    _cachedPage = (viewportDimension == 0.0) ? page : null;

461 462 463 464 465 466
    if (newPixels != oldPixels) {
      correctPixels(newPixels);
      return false;
    }
    return result;
  }
467

468 469 470 471 472 473 474 475 476 477 478 479 480 481
  @override
  void absorb(ScrollPosition other) {
    super.absorb(other);
    assert(_cachedPage == null);

    if (other is! _PagePosition) {
      return;
    }

    if (other._cachedPage != null) {
      _cachedPage = other._cachedPage;
    }
  }

482 483 484 485 486 487 488 489 490
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
    return super.applyContentDimensions(
      newMinScrollExtent,
      math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
    );
  }

491
  @override
492
  PageMetrics copyWith({
493 494 495 496 497 498
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? viewportFraction,
499
    double? devicePixelRatio,
500
  }) {
501
    return PageMetrics(
502 503 504 505
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
506 507
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
508
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
509 510 511 512
    );
  }
}

513 514
class _ForceImplicitScrollPhysics extends ScrollPhysics {
  const _ForceImplicitScrollPhysics({
515
    required this.allowImplicitScrolling,
516 517
    super.parent,
  }) : assert(allowImplicitScrolling != null);
518 519

  @override
520
  _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
521 522 523 524 525 526 527 528 529 530
    return _ForceImplicitScrollPhysics(
      allowImplicitScrolling: allowImplicitScrolling,
      parent: buildParent(ancestor),
    );
  }

  @override
  final bool allowImplicitScrolling;
}

531 532 533
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
534 535 536 537 538 539
///
/// 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.
540 541
class PageScrollPhysics extends ScrollPhysics {
  /// Creates physics for a [PageView].
542
  const PageScrollPhysics({ super.parent });
543 544

  @override
545
  PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
546
    return PageScrollPhysics(parent: buildParent(ancestor));
547
  }
548

549
  double _getPage(ScrollMetrics position) {
550
    if (position is _PagePosition) {
551
      return position.page!;
552
    }
553 554 555
    return position.pixels / position.viewportDimension;
  }

556
  double _getPixels(ScrollMetrics position, double page) {
557
    if (position is _PagePosition) {
558
      return position.getPixelsFromPage(page);
559
    }
560 561 562
    return page * position.viewportDimension;
  }

563 564
  double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
565
    if (velocity < -tolerance.velocity) {
566
      page -= 0.5;
567
    } else if (velocity > tolerance.velocity) {
568
      page += 0.5;
569
    }
570
    return _getPixels(position, page.roundToDouble());
571 572 573
  }

  @override
574
  Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
575 576 577
    // 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) ||
578
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
579
      return super.createBallisticSimulation(position, velocity);
580
    }
581
    final Tolerance tolerance = toleranceFor(position);
582
    final double target = _getTargetPixels(position, tolerance, velocity);
583
    if (target != position.pixels) {
584
      return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
585
    }
586
    return null;
587
  }
588 589 590

  @override
  bool get allowImplicitScrolling => false;
591 592 593 594 595 596
}

// 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.
597
final PageController _defaultPageController = PageController();
598
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
599 600

/// A scrollable list that works page by page.
Adam Barth's avatar
Adam Barth committed
601 602 603 604 605 606 607 608 609 610 611 612
///
/// 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.
613
///
614 615
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
616
/// {@tool dartpad}
617 618 619
/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally.
///
620
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
621 622
/// {@end-tool}
///
623 624
/// See also:
///
Adam Barth's avatar
Adam Barth committed
625 626 627 628
///  * [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.
629
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
630
///    the scroll position without using a [ScrollController].
631
class PageView extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
632 633 634 635 636 637 638
  /// 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.
639
  ///
640 641 642 643
  /// 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.
  ///
644
  /// {@template flutter.widgets.PageView.allowImplicitScrolling}
645 646 647 648 649
  /// 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}
650
  PageView({
651
    super.key,
652 653
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
654
    PageController? controller,
655
    this.physics,
656
    this.pageSnapping = true,
657
    this.onPageChanged,
658
    List<Widget> children = const <Widget>[],
659
    this.dragStartBehavior = DragStartBehavior.start,
660
    this.allowImplicitScrolling = false,
661
    this.restorationId,
662
    this.clipBehavior = Clip.hardEdge,
663
    this.scrollBehavior,
664
    this.padEnds = true,
665
  }) : assert(allowImplicitScrolling != null),
666
       assert(clipBehavior != null),
667
       controller = controller ?? _defaultPageController,
668
       childrenDelegate = SliverChildListDelegate(children);
669

Adam Barth's avatar
Adam Barth committed
670 671 672 673 674 675 676 677 678 679 680 681
  /// 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].
682
  ///
683 684
  /// {@macro flutter.widgets.ListView.builder.itemBuilder}
  ///
685 686 687 688 689 690 691 692
  /// {@template flutter.widgets.PageView.findChildIndexCallback}
  /// The [findChildIndexCallback] corresponds to the
  /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
  /// a child widget may not map to its existing [RenderObject] when the order
  /// of children returned from the children builder changes.
  /// This may result in state-loss. This callback needs to be implemented if
  /// the order of the children may change at a later time.
  /// {@endtemplate}
693
  ///
694
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
695
  PageView.builder({
696
    super.key,
697 698
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
699
    PageController? controller,
700
    this.physics,
701
    this.pageSnapping = true,
702
    this.onPageChanged,
703
    required NullableIndexedWidgetBuilder itemBuilder,
704
    ChildIndexGetter? findChildIndexCallback,
705
    int? itemCount,
706
    this.dragStartBehavior = DragStartBehavior.start,
707
    this.allowImplicitScrolling = false,
708
    this.restorationId,
709
    this.clipBehavior = Clip.hardEdge,
710
    this.scrollBehavior,
711
    this.padEnds = true,
712
  }) : assert(allowImplicitScrolling != null),
713
       assert(clipBehavior != null),
714
       controller = controller ?? _defaultPageController,
715 716 717 718
       childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         findChildIndexCallback: findChildIndexCallback,
         childCount: itemCount,
719
       );
720

Adam Barth's avatar
Adam Barth committed
721 722
  /// Creates a scrollable list that works page by page with a custom child
  /// model.
723
  ///
724
  /// {@tool snippet}
725 726 727 728 729 730
  ///
  /// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child
  /// reordering.
  ///
  /// ```dart
  /// class MyPageView extends StatefulWidget {
731
  ///   const MyPageView({super.key});
732
  ///
733
  ///   @override
734
  ///   State<MyPageView> createState() => _MyPageViewState();
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759
  /// }
  ///
  /// 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) {
760
  ///               final ValueKey<String> valueKey = key as ValueKey<String>;
761 762 763 764 765 766 767 768 769 770
  ///               final String data = valueKey.value;
  ///               return items.indexOf(data);
  ///             }
  ///           ),
  ///         ),
  ///       ),
  ///       bottomNavigationBar: BottomAppBar(
  ///         child: Row(
  ///           mainAxisAlignment: MainAxisAlignment.center,
  ///           children: <Widget>[
771
  ///             TextButton(
772
  ///               onPressed: () => _reverse(),
773
  ///               child: const Text('Reverse items'),
774 775 776 777 778 779 780 781 782
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  ///
  /// class KeepAlive extends StatefulWidget {
783
  ///   const KeepAlive({super.key, required this.data});
784 785 786 787
  ///
  ///   final String data;
  ///
  ///   @override
788
  ///   State<KeepAlive> createState() => _KeepAliveState();
789 790 791 792 793 794 795 796 797 798 799 800 801 802
  /// }
  ///
  /// 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}
803
  ///
804
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
805
  PageView.custom({
806
    super.key,
807 808
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
809
    PageController? controller,
810
    this.physics,
811
    this.pageSnapping = true,
812
    this.onPageChanged,
813
    required this.childrenDelegate,
814
    this.dragStartBehavior = DragStartBehavior.start,
815
    this.allowImplicitScrolling = false,
816
    this.restorationId,
817
    this.clipBehavior = Clip.hardEdge,
818
    this.scrollBehavior,
819
    this.padEnds = true,
820
  }) : assert(childrenDelegate != null),
821
       assert(allowImplicitScrolling != null),
822
       assert(clipBehavior != null),
823
       controller = controller ?? _defaultPageController;
824

825 826 827 828 829 830 831 832 833 834 835 836 837
  /// 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;

838
  /// {@macro flutter.widgets.scrollable.restorationId}
839
  final String? restorationId;
840

Adam Barth's avatar
Adam Barth committed
841 842 843
  /// The axis along which the page view scrolls.
  ///
  /// Defaults to [Axis.horizontal].
844 845
  final Axis scrollDirection;

Adam Barth's avatar
Adam Barth committed
846 847 848 849 850 851 852 853 854 855 856 857
  /// 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.
858 859
  final bool reverse;

Adam Barth's avatar
Adam Barth committed
860 861
  /// An object that can be used to control the position to which this page
  /// view is scrolled.
862 863
  final PageController controller;

Adam Barth's avatar
Adam Barth committed
864 865 866 867 868 869 870 871
  /// 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.
  ///
872 873 874 875
  /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
  /// [ScrollPhysics] provided by that behavior will take precedence after
  /// [physics].
  ///
Adam Barth's avatar
Adam Barth committed
876
  /// Defaults to matching platform conventions.
877
  final ScrollPhysics? physics;
878

879
  /// Set to false to disable page snapping, useful for custom scroll behavior.
880 881 882 883
  ///
  /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
  /// the page will snap to the beginning of the viewport; otherwise, the page
  /// will snap to the center of the viewport.
884 885
  final bool pageSnapping;

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

Adam Barth's avatar
Adam Barth committed
889 890 891 892 893 894
  /// 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.
895 896
  final SliverChildDelegate childrenDelegate;

897 898 899
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

900
  /// {@macro flutter.material.Material.clipBehavior}
901 902 903 904
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

905 906 907 908 909 910 911 912 913 914 915
  /// {@macro flutter.widgets.shadow.scrollBehavior}
  ///
  /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
  /// [ScrollPhysics] is provided in [physics], it will take precedence,
  /// followed by [scrollBehavior], and then the inherited ancestor
  /// [ScrollBehavior].
  ///
  /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
  /// modified by default to not apply a [Scrollbar].
  final ScrollBehavior? scrollBehavior;

916 917 918 919 920 921 922 923 924 925 926
  /// Whether to add padding to both ends of the list.
  ///
  /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
  /// such that the first and last child slivers will be in the center of
  /// the viewport when scrolled all the way to the start or end, respectively.
  ///
  /// If [PageController.viewportFraction] >= 1.0, this property has no effect.
  ///
  /// This property defaults to true and must not be null.
  final bool padEnds;

927
  @override
928
  State<PageView> createState() => _PageViewState();
929 930 931 932 933 934 935 936
}

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

  @override
  void initState() {
    super.initState();
937
    _lastReportedPage = widget.controller.initialPage;
938 939 940
  }

  AxisDirection _getDirection(BuildContext context) {
941
    switch (widget.scrollDirection) {
942
      case Axis.horizontal:
943
        assert(debugCheckHasDirectionality(context));
944
        final TextDirection textDirection = Directionality.of(context);
945 946
        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
        return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
947
      case Axis.vertical:
948
        return widget.reverse ? AxisDirection.up : AxisDirection.down;
949
    }
950 951 952 953
  }

  @override
  Widget build(BuildContext context) {
954
    final AxisDirection axisDirection = _getDirection(context);
955 956
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
957 958
    ).applyTo(
      widget.pageSnapping
959
        ? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
960 961
        : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
    );
962

963
    return NotificationListener<ScrollNotification>(
Adam Barth's avatar
Adam Barth committed
964
      onNotification: (ScrollNotification notification) {
965
        if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
966
          final PageMetrics metrics = notification.metrics as PageMetrics;
967
          final int currentPage = metrics.page!.round();
968 969
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
970
            widget.onPageChanged!(currentPage);
971
          }
972
        }
973
        return false;
974
      },
975
      child: Scrollable(
976
        dragStartBehavior: widget.dragStartBehavior,
977
        axisDirection: axisDirection,
978
        controller: widget.controller,
979
        physics: physics,
980
        restorationId: widget.restorationId,
981
        scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
982
        viewportBuilder: (BuildContext context, ViewportOffset position) {
983
          return Viewport(
984 985 986 987 988
            // 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,
989
            axisDirection: axisDirection,
990
            offset: position,
991
            clipBehavior: widget.clipBehavior,
992
            slivers: <Widget>[
993
              SliverFillViewport(
994
                viewportFraction: widget.controller.viewportFraction,
995
                delegate: widget.childrenDelegate,
996
                padEnds: widget.padEnds,
997
              ),
998 999 1000 1001
            ],
          );
        },
      ),
1002 1003
    );
  }
1004 1005

  @override
1006
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
1007
    super.debugFillProperties(description);
1008 1009 1010 1011 1012
    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'));
1013
    description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
1014
  }
1015
}