page_view.dart 32.3 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/physics.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/gestures.dart' show DragStartBehavior;
10
import 'package:flutter/foundation.dart' show precisionErrorTolerance;
11 12

import 'basic.dart';
13
import 'debug.dart';
14 15
import 'framework.dart';
import 'notification_listener.dart';
16
import 'page_storage.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 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
/// 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(
79
///                 child: ElevatedButton(
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         1,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
///                   child: Text('Next'),
///                 ),
///               ),
///             ),
///             Container(
///               color: Colors.blue,
///               child: Center(
96
///                 child: ElevatedButton(
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
///                   onPressed: () {
///                     if (_pageController.hasClients) {
///                       _pageController.animateToPage(
///                         0,
///                         duration: const Duration(milliseconds: 400),
///                         curve: Curves.easeInOut,
///                       );
///                     }
///                   },
///                   child: Text('Previous'),
///                 ),
///               ),
///             ),
///           ],
///         ),
///       ),
///     );
///   }
/// }
///
/// ```
/// {@end-tool}
119
class PageController extends ScrollController {
120 121
  /// Creates a page controller.
  ///
122
  /// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null.
123
  PageController({
124 125 126
    this.initialPage = 0,
    this.keepPage = true,
    this.viewportFraction = 1.0,
127
  }) : assert(initialPage != null),
128
       assert(keepPage != null),
129 130
       assert(viewportFraction != null),
       assert(viewportFraction > 0.0);
131

132
  /// The page to show when first creating the [PageView].
133
  final int initialPage;
134

135 136 137 138 139 140 141 142 143 144 145 146 147
  /// 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
148
  ///    scrollable appears in the same route, to distinguish the [PageStorage]
149 150 151
  ///    locations used to save scroll offsets.
  final bool keepPage;

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

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

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

217 218 219 220 221 222
  /// 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.
223 224
  Future<void> nextPage({ required Duration duration, required Curve curve }) {
    return animateToPage(page!.round() + 1, duration: duration, curve: curve);
225 226
  }

227 228 229 230 231 232
  /// 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.
233 234
  Future<void> previousPage({ required Duration duration, required Curve curve }) {
    return animateToPage(page!.round() - 1, duration: duration, curve: curve);
235 236 237
  }

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

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

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

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

  /// The current page displayed in the [PageView].
298
  double? get page {
299 300 301 302 303 304 305 306
    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;
307 308
}

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

331
  final int initialPage;
332
  double _pageToUseOnStartup;
333

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

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

369 370 371 372 373 374 375 376
  // 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);

377
  double getPageFromPixels(double pixels, double viewportDimension) {
378
    final double actual = math.max(0.0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction);
379 380 381 382 383
    final double round = actual.roundToDouble();
    if ((actual - round).abs() < precisionErrorTolerance) {
      return round;
    }
    return actual;
384 385 386
  }

  double getPixelsFromPage(double page) {
387
    return page * viewportDimension * viewportFraction + _initialPageOffset;
388 389
  }

390
  @override
391
  double? get page {
392
    assert(
393
      !hasPixels || (minScrollExtent != null && maxScrollExtent != null),
394 395
      'Page value is only available after content dimensions are established.',
    );
396
    return !hasPixels ? null : getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
397
  }
398

399 400 401 402 403 404 405
  @override
  void saveScrollOffset() {
    PageStorage.of(context.storageContext)?.writeState(context.storageContext, getPageFromPixels(pixels, viewportDimension));
  }

  @override
  void restoreScrollOffset() {
406 407
    if (!hasPixels) {
      final double? value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double?;
408
      if (value != null)
409
        _pageToUseOnStartup = value;
410 411 412
    }
  }

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
  @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));
    }
  }

429 430
  @override
  bool applyViewportDimension(double viewportDimension) {
431
    final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
432 433 434
    if (viewportDimension == oldViewportDimensions) {
      return true;
    }
435
    final bool result = super.applyViewportDimension(viewportDimension);
436 437
    final double? oldPixels = hasPixels ? pixels : null;
    final double page = (oldPixels == null || oldViewportDimensions == 0.0) ? _pageToUseOnStartup : getPageFromPixels(oldPixels, oldViewportDimensions!);
438
    final double newPixels = getPixelsFromPage(page);
439

440 441 442 443 444 445
    if (newPixels != oldPixels) {
      correctPixels(newPixels);
      return false;
    }
    return result;
  }
446

447 448 449 450 451 452 453 454 455
  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
    return super.applyContentDimensions(
      newMinScrollExtent,
      math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
    );
  }

456
  @override
457
  PageMetrics copyWith({
458 459 460 461 462 463
    double? minScrollExtent,
    double? maxScrollExtent,
    double? pixels,
    double? viewportDimension,
    AxisDirection? axisDirection,
    double? viewportFraction,
464
  }) {
465
    return PageMetrics(
466 467 468 469
      minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
      maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
      pixels: pixels ?? (hasPixels ? this.pixels : null),
      viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
470 471
      axisDirection: axisDirection ?? this.axisDirection,
      viewportFraction: viewportFraction ?? this.viewportFraction,
472 473 474 475
    );
  }
}

476 477
class _ForceImplicitScrollPhysics extends ScrollPhysics {
  const _ForceImplicitScrollPhysics({
478 479
    required this.allowImplicitScrolling,
    ScrollPhysics? parent,
480 481 482 483
  }) : assert(allowImplicitScrolling != null),
       super(parent: parent);

  @override
484
  _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
485 486 487 488 489 490 491 492 493 494
    return _ForceImplicitScrollPhysics(
      allowImplicitScrolling: allowImplicitScrolling,
      parent: buildParent(ancestor),
    );
  }

  @override
  final bool allowImplicitScrolling;
}

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

  @override
509
  PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
510
    return PageScrollPhysics(parent: buildParent(ancestor));
511
  }
512

513
  double _getPage(ScrollMetrics position) {
514
    if (position is _PagePosition)
515
      return position.page!;
516 517 518
    return position.pixels / position.viewportDimension;
  }

519
  double _getPixels(ScrollMetrics position, double page) {
520 521 522 523 524
    if (position is _PagePosition)
      return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

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

  @override
  bool get allowImplicitScrolling => false;
550 551 552 553 554 555
}

// 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.
556
final PageController _defaultPageController = PageController();
557
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
558 559

/// A scrollable list that works page by page.
Adam Barth's avatar
Adam Barth committed
560 561 562 563 564 565 566 567 568 569 570 571
///
/// 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.
572
///
573 574
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
575 576
/// See also:
///
Adam Barth's avatar
Adam Barth committed
577 578 579 580
///  * [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.
581
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
582
///    the scroll position without using a [ScrollController].
583
class PageView extends StatefulWidget {
Adam Barth's avatar
Adam Barth committed
584 585 586 587 588 589 590
  /// 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.
591
  ///
592 593 594 595
  /// 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.
  ///
596
  /// {@template flutter.widgets.PageView.allowImplicitScrolling}
597 598 599 600 601
  /// 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}
602
  PageView({
603
    Key? key,
604 605
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
606
    PageController? controller,
607
    this.physics,
608
    this.pageSnapping = true,
609
    this.onPageChanged,
610
    List<Widget> children = const <Widget>[],
611
    this.dragStartBehavior = DragStartBehavior.start,
612
    this.allowImplicitScrolling = false,
613
    this.restorationId,
614
    this.clipBehavior = Clip.hardEdge,
615
  }) : assert(allowImplicitScrolling != null),
616
       assert(clipBehavior != null),
617
       controller = controller ?? _defaultPageController,
618
       childrenDelegate = SliverChildListDelegate(children),
619
       super(key: key);
620

Adam Barth's avatar
Adam Barth committed
621 622 623 624 625 626 627 628 629 630 631 632
  /// 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].
633 634 635 636
  ///
  /// [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].
637
  ///
638
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
639
  PageView.builder({
640
    Key? key,
641 642
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
643
    PageController? controller,
644
    this.physics,
645
    this.pageSnapping = true,
646
    this.onPageChanged,
647 648
    required IndexedWidgetBuilder itemBuilder,
    int? itemCount,
649
    this.dragStartBehavior = DragStartBehavior.start,
650
    this.allowImplicitScrolling = false,
651
    this.restorationId,
652
    this.clipBehavior = Clip.hardEdge,
653
  }) : assert(allowImplicitScrolling != null),
654
       assert(clipBehavior != null),
655
       controller = controller ?? _defaultPageController,
656
       childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
657
       super(key: key);
658

Adam Barth's avatar
Adam Barth committed
659 660
  /// Creates a scrollable list that works page by page with a custom child
  /// model.
661
  ///
662
  /// {@tool snippet}
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 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
  ///
  /// 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>[
707
  ///             TextButton(
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
  ///               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}
739
  ///
740
  /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
741
  PageView.custom({
742
    Key? key,
743 744
    this.scrollDirection = Axis.horizontal,
    this.reverse = false,
745
    PageController? controller,
746
    this.physics,
747
    this.pageSnapping = true,
748
    this.onPageChanged,
749
    required this.childrenDelegate,
750
    this.dragStartBehavior = DragStartBehavior.start,
751
    this.allowImplicitScrolling = false,
752
    this.restorationId,
753
    this.clipBehavior = Clip.hardEdge,
754
  }) : assert(childrenDelegate != null),
755
       assert(allowImplicitScrolling != null),
756
       assert(clipBehavior != null),
757 758
       controller = controller ?? _defaultPageController,
       super(key: key);
759

760 761 762 763 764 765 766 767 768 769 770 771 772
  /// 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;

773
  /// {@macro flutter.widgets.scrollable.restorationId}
774
  final String? restorationId;
775

Adam Barth's avatar
Adam Barth committed
776 777 778
  /// The axis along which the page view scrolls.
  ///
  /// Defaults to [Axis.horizontal].
779 780
  final Axis scrollDirection;

Adam Barth's avatar
Adam Barth committed
781 782 783 784 785 786 787 788 789 790 791 792
  /// 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.
793 794
  final bool reverse;

Adam Barth's avatar
Adam Barth committed
795 796
  /// An object that can be used to control the position to which this page
  /// view is scrolled.
797 798
  final PageController controller;

Adam Barth's avatar
Adam Barth committed
799 800 801 802 803 804 805 806 807
  /// 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.
808
  final ScrollPhysics? physics;
809

810 811 812
  /// Set to false to disable page snapping, useful for custom scroll behavior.
  final bool pageSnapping;

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

Adam Barth's avatar
Adam Barth committed
816 817 818 819 820 821
  /// 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.
822 823
  final SliverChildDelegate childrenDelegate;

824 825 826
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

827
  /// {@macro flutter.material.Material.clipBehavior}
828 829 830 831
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

832
  @override
833
  _PageViewState createState() => _PageViewState();
834 835 836 837 838 839 840 841
}

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

  @override
  void initState() {
    super.initState();
842
    _lastReportedPage = widget.controller.initialPage;
843 844 845
  }

  AxisDirection _getDirection(BuildContext context) {
846
    switch (widget.scrollDirection) {
847
      case Axis.horizontal:
848
        assert(debugCheckHasDirectionality(context));
849
        final TextDirection textDirection = Directionality.of(context);
850 851
        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
        return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
852
      case Axis.vertical:
853
        return widget.reverse ? AxisDirection.up : AxisDirection.down;
854
    }
855 856 857 858
  }

  @override
  Widget build(BuildContext context) {
859
    final AxisDirection axisDirection = _getDirection(context);
860 861 862
    final ScrollPhysics physics = _ForceImplicitScrollPhysics(
      allowImplicitScrolling: widget.allowImplicitScrolling,
    ).applyTo(widget.pageSnapping
863
        ? _kPagePhysics.applyTo(widget.physics)
864
        : widget.physics);
865

866
    return NotificationListener<ScrollNotification>(
Adam Barth's avatar
Adam Barth committed
867
      onNotification: (ScrollNotification notification) {
868
        if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
869
          final PageMetrics metrics = notification.metrics as PageMetrics;
870
          final int currentPage = metrics.page!.round();
871 872
          if (currentPage != _lastReportedPage) {
            _lastReportedPage = currentPage;
873
            widget.onPageChanged!(currentPage);
874
          }
875
        }
876
        return false;
877
      },
878
      child: Scrollable(
879
        dragStartBehavior: widget.dragStartBehavior,
880
        axisDirection: axisDirection,
881
        controller: widget.controller,
882
        physics: physics,
883
        restorationId: widget.restorationId,
884
        viewportBuilder: (BuildContext context, ViewportOffset position) {
885
          return Viewport(
886 887 888 889 890
            // 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,
891
            axisDirection: axisDirection,
892
            offset: position,
893
            clipBehavior: widget.clipBehavior,
894
            slivers: <Widget>[
895
              SliverFillViewport(
896
                viewportFraction: widget.controller.viewportFraction,
897
                delegate: widget.childrenDelegate,
898
              ),
899 900 901 902
            ],
          );
        },
      ),
903 904
    );
  }
905 906

  @override
907
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
908
    super.debugFillProperties(description);
909 910 911 912 913
    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'));
914
    description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
915
  }
916
}