1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/foundation.dart' show clampDouble, precisionErrorTolerance;
8import 'package:flutter/gestures.dart' show DragStartBehavior;
9import 'package:flutter/rendering.dart';
10
11import 'basic.dart';
12import 'debug.dart';
13import 'framework.dart';
14import 'notification_listener.dart';
15import 'page_storage.dart';
16import 'scroll_configuration.dart';
17import 'scroll_context.dart';
18import 'scroll_controller.dart';
19import 'scroll_delegate.dart';
20import 'scroll_metrics.dart';
21import 'scroll_notification.dart';
22import 'scroll_physics.dart';
23import 'scroll_position.dart';
24import 'scroll_position_with_single_context.dart';
25import 'scroll_view.dart';
26import 'scrollable.dart';
27import 'sliver_fill.dart';
28import 'viewport.dart';
29
30/// A controller for [PageView].
31///
32/// A page controller lets you manipulate which page is visible in a [PageView].
33/// In addition to being able to control the pixel offset of the content inside
34/// the [PageView], a [PageController] also lets you control the offset in terms
35/// of pages, which are increments of the viewport size.
36///
37/// See also:
38///
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/// to animate the [PageView] using a [PageController].
46///
47/// ```dart
48/// 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/// @override
59/// void dispose() {
60/// _pageController.dispose();
61/// super.dispose();
62/// }
63///
64/// @override
65/// Widget build(BuildContext context) {
66/// return MaterialApp(
67/// home: Scaffold(
68/// body: PageView(
69/// controller: _pageController,
70/// children: <Widget>[
71/// ColoredBox(
72/// color: Colors.red,
73/// child: Center(
74/// child: ElevatedButton(
75/// onPressed: () {
76/// if (_pageController.hasClients) {
77/// _pageController.animateToPage(
78/// 1,
79/// duration: const Duration(milliseconds: 400),
80/// curve: Curves.easeInOut,
81/// );
82/// }
83/// },
84/// child: const Text('Next'),
85/// ),
86/// ),
87/// ),
88/// ColoredBox(
89/// color: Colors.blue,
90/// child: Center(
91/// child: ElevatedButton(
92/// onPressed: () {
93/// if (_pageController.hasClients) {
94/// _pageController.animateToPage(
95/// 0,
96/// duration: const Duration(milliseconds: 400),
97/// curve: Curves.easeInOut,
98/// );
99/// }
100/// },
101/// child: const Text('Previous'),
102/// ),
103/// ),
104/// ),
105/// ],
106/// ),
107/// ),
108/// );
109/// }
110/// }
111/// ```
112/// {@end-tool}
113class PageController extends ScrollController {
114 /// Creates a page controller.
115 PageController({
116 this.initialPage = 0,
117 this.keepPage = true,
118 this.viewportFraction = 1.0,
119 super.onAttach,
120 super.onDetach,
121 }) : assert(viewportFraction > 0.0);
122
123 /// The page to show when first creating the [PageView].
124 final int initialPage;
125
126 /// Save the current [page] with [PageStorage] and restore it if
127 /// this controller's scrollable is recreated.
128 ///
129 /// If this property is set to false, the current [page] is never saved
130 /// and [initialPage] is always used to initialize the scroll offset.
131 /// If true (the default), the initial page is used the first time the
132 /// controller's scrollable is created, since there's isn't a page to
133 /// restore yet. Subsequently the saved page is restored and
134 /// [initialPage] is ignored.
135 ///
136 /// See also:
137 ///
138 /// * [PageStorageKey], which should be used when more than one
139 /// scrollable appears in the same route, to distinguish the [PageStorage]
140 /// locations used to save scroll offsets.
141 final bool keepPage;
142
143 /// {@template flutter.widgets.pageview.viewportFraction}
144 /// The fraction of the viewport that each page should occupy.
145 ///
146 /// Defaults to 1.0, which means each page fills the viewport in the scrolling
147 /// direction.
148 /// {@endtemplate}
149 final double viewportFraction;
150
151 /// The current page displayed in the controlled [PageView].
152 ///
153 /// There are circumstances that this [PageController] can't know the current
154 /// page. Reading [page] will throw an [AssertionError] in the following cases:
155 ///
156 /// 1. No [PageView] is currently using this [PageController]. Once a
157 /// [PageView] starts using this [PageController], the new [page]
158 /// position will be derived:
159 ///
160 /// * First, based on the attached [PageView]'s [BuildContext] and the
161 /// position saved at that context's [PageStorage] if [keepPage] is true.
162 /// * Second, from the [PageController]'s [initialPage].
163 ///
164 /// 2. More than one [PageView] using the same [PageController].
165 ///
166 /// The [hasClients] property can be used to check if a [PageView] is attached
167 /// prior to accessing [page].
168 double? get page {
169 assert(
170 positions.isNotEmpty,
171 'PageController.page cannot be accessed before a PageView is built with it.',
172 );
173 assert(
174 positions.length == 1,
175 'The page property cannot be read when multiple PageViews are attached to '
176 'the same PageController.',
177 );
178 final _PagePosition position = this.position as _PagePosition;
179 return position.page;
180 }
181
182 /// Animates the controlled [PageView] from the current page to the given page.
183 ///
184 /// The animation lasts for the given duration and follows the given curve.
185 /// The returned [Future] resolves when the animation completes.
186 Future<void> animateToPage(
187 int page, {
188 required Duration duration,
189 required Curve curve,
190 }) {
191 final _PagePosition position = this.position as _PagePosition;
192 if (position._cachedPage != null) {
193 position._cachedPage = page.toDouble();
194 return Future<void>.value();
195 }
196
197 return position.animateTo(
198 position.getPixelsFromPage(page.toDouble()),
199 duration: duration,
200 curve: curve,
201 );
202 }
203
204 /// Changes which page is displayed in the controlled [PageView].
205 ///
206 /// Jumps the page position from its current value to the given value,
207 /// without animation, and without checking if the new value is in range.
208 void jumpToPage(int page) {
209 final _PagePosition position = this.position as _PagePosition;
210 if (position._cachedPage != null) {
211 position._cachedPage = page.toDouble();
212 return;
213 }
214
215 position.jumpTo(position.getPixelsFromPage(page.toDouble()));
216 }
217
218 /// Animates the controlled [PageView] to the next page.
219 ///
220 /// The animation lasts for the given duration and follows the given curve.
221 /// The returned [Future] resolves when the animation completes.
222 Future<void> nextPage({ required Duration duration, required Curve curve }) {
223 return animateToPage(page!.round() + 1, duration: duration, curve: curve);
224 }
225
226 /// Animates the controlled [PageView] to the previous page.
227 ///
228 /// The animation lasts for the given duration and follows the given curve.
229 /// The returned [Future] resolves when the animation completes.
230 Future<void> previousPage({ required Duration duration, required Curve curve }) {
231 return animateToPage(page!.round() - 1, duration: duration, curve: curve);
232 }
233
234 @override
235 ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
236 return _PagePosition(
237 physics: physics,
238 context: context,
239 initialPage: initialPage,
240 keepPage: keepPage,
241 viewportFraction: viewportFraction,
242 oldPosition: oldPosition,
243 );
244 }
245
246 @override
247 void attach(ScrollPosition position) {
248 super.attach(position);
249 final _PagePosition pagePosition = position as _PagePosition;
250 pagePosition.viewportFraction = viewportFraction;
251 }
252}
253
254/// Metrics for a [PageView].
255///
256/// The metrics are available on [ScrollNotification]s generated from
257/// [PageView]s.
258class PageMetrics extends FixedScrollMetrics {
259 /// Creates an immutable snapshot of values associated with a [PageView].
260 PageMetrics({
261 required super.minScrollExtent,
262 required super.maxScrollExtent,
263 required super.pixels,
264 required super.viewportDimension,
265 required super.axisDirection,
266 required this.viewportFraction,
267 required super.devicePixelRatio,
268 });
269
270 @override
271 PageMetrics copyWith({
272 double? minScrollExtent,
273 double? maxScrollExtent,
274 double? pixels,
275 double? viewportDimension,
276 AxisDirection? axisDirection,
277 double? viewportFraction,
278 double? devicePixelRatio,
279 }) {
280 return PageMetrics(
281 minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
282 maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
283 pixels: pixels ?? (hasPixels ? this.pixels : null),
284 viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
285 axisDirection: axisDirection ?? this.axisDirection,
286 viewportFraction: viewportFraction ?? this.viewportFraction,
287 devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
288 );
289 }
290
291 /// The current page displayed in the [PageView].
292 double? get page {
293 return math.max(0.0, clampDouble(pixels, minScrollExtent, maxScrollExtent)) /
294 math.max(1.0, viewportDimension * viewportFraction);
295 }
296
297 /// The fraction of the viewport that each page occupies.
298 ///
299 /// Used to compute [page] from the current [pixels].
300 final double viewportFraction;
301}
302
303class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
304 _PagePosition({
305 required super.physics,
306 required super.context,
307 this.initialPage = 0,
308 bool keepPage = true,
309 double viewportFraction = 1.0,
310 super.oldPosition,
311 }) : assert(viewportFraction > 0.0),
312 _viewportFraction = viewportFraction,
313 _pageToUseOnStartup = initialPage.toDouble(),
314 super(
315 initialPixels: null,
316 keepScrollOffset: keepPage,
317 );
318
319 final int initialPage;
320 double _pageToUseOnStartup;
321 // When the viewport has a zero-size, the `page` can not
322 // be retrieved by `getPageFromPixels`, so we need to cache the page
323 // for use when resizing the viewport to non-zero next time.
324 double? _cachedPage;
325
326 @override
327 Future<void> ensureVisible(
328 RenderObject object, {
329 double alignment = 0.0,
330 Duration duration = Duration.zero,
331 Curve curve = Curves.ease,
332 ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
333 RenderObject? targetRenderObject,
334 }) {
335 // Since the _PagePosition is intended to cover the available space within
336 // its viewport, stop trying to move the target render object to the center
337 // - otherwise, could end up changing which page is visible and moving the
338 // targetRenderObject out of the viewport.
339 return super.ensureVisible(
340 object,
341 alignment: alignment,
342 duration: duration,
343 curve: curve,
344 alignmentPolicy: alignmentPolicy,
345 );
346 }
347
348 @override
349 double get viewportFraction => _viewportFraction;
350 double _viewportFraction;
351 set viewportFraction(double value) {
352 if (_viewportFraction == value) {
353 return;
354 }
355 final double? oldPage = page;
356 _viewportFraction = value;
357 if (oldPage != null) {
358 forcePixels(getPixelsFromPage(oldPage));
359 }
360 }
361
362 // The amount of offset that will be added to [minScrollExtent] and subtracted
363 // from [maxScrollExtent], such that every page will properly snap to the center
364 // of the viewport when viewportFraction is greater than 1.
365 //
366 // The value is 0 if viewportFraction is less than or equal to 1, larger than 0
367 // otherwise.
368 double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);
369
370 double getPageFromPixels(double pixels, double viewportDimension) {
371 assert(viewportDimension > 0.0);
372 final double actual = math.max(0.0, pixels - _initialPageOffset) / (viewportDimension * viewportFraction);
373 final double round = actual.roundToDouble();
374 if ((actual - round).abs() < precisionErrorTolerance) {
375 return round;
376 }
377 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 || hasContentDimensions,
388 'Page value is only available after content dimensions are established.',
389 );
390 return !hasPixels || !hasContentDimensions
391 ? null
392 : _cachedPage ?? getPageFromPixels(clampDouble(pixels, minScrollExtent, maxScrollExtent), viewportDimension);
393 }
394
395 @override
396 void saveScrollOffset() {
397 PageStorage.maybeOf(context.storageContext)?.writeState(context.storageContext, _cachedPage ?? getPageFromPixels(pixels, viewportDimension));
398 }
399
400 @override
401 void restoreScrollOffset() {
402 if (!hasPixels) {
403 final double? value = PageStorage.maybeOf(context.storageContext)?.readState(context.storageContext) as double?;
404 if (value != null) {
405 _pageToUseOnStartup = value;
406 }
407 }
408 }
409
410 @override
411 void saveOffset() {
412 context.saveOffset(_cachedPage ?? getPageFromPixels(pixels, viewportDimension));
413 }
414
415 @override
416 void restoreOffset(double offset, {bool initialRestore = false}) {
417 if (initialRestore) {
418 _pageToUseOnStartup = offset;
419 } else {
420 jumpTo(getPixelsFromPage(offset));
421 }
422 }
423
424 @override
425 bool applyViewportDimension(double viewportDimension) {
426 final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
427 if (viewportDimension == oldViewportDimensions) {
428 return true;
429 }
430 final bool result = super.applyViewportDimension(viewportDimension);
431 final double? oldPixels = hasPixels ? pixels : null;
432 double page;
433 if (oldPixels == null) {
434 page = _pageToUseOnStartup;
435 } else if (oldViewportDimensions == 0.0) {
436 // If resize from zero, we should use the _cachedPage to recover the state.
437 page = _cachedPage!;
438 } else {
439 page = getPageFromPixels(oldPixels, oldViewportDimensions!);
440 }
441 final double newPixels = getPixelsFromPage(page);
442
443 // If the viewportDimension is zero, cache the page
444 // in case the viewport is resized to be non-zero.
445 _cachedPage = (viewportDimension == 0.0) ? page : null;
446
447 if (newPixels != oldPixels) {
448 correctPixels(newPixels);
449 return false;
450 }
451 return result;
452 }
453
454 @override
455 void absorb(ScrollPosition other) {
456 super.absorb(other);
457 assert(_cachedPage == null);
458
459 if (other is! _PagePosition) {
460 return;
461 }
462
463 if (other._cachedPage != null) {
464 _cachedPage = other._cachedPage;
465 }
466 }
467
468 @override
469 bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
470 final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
471 return super.applyContentDimensions(
472 newMinScrollExtent,
473 math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
474 );
475 }
476
477 @override
478 PageMetrics copyWith({
479 double? minScrollExtent,
480 double? maxScrollExtent,
481 double? pixels,
482 double? viewportDimension,
483 AxisDirection? axisDirection,
484 double? viewportFraction,
485 double? devicePixelRatio,
486 }) {
487 return PageMetrics(
488 minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
489 maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
490 pixels: pixels ?? (hasPixels ? this.pixels : null),
491 viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
492 axisDirection: axisDirection ?? this.axisDirection,
493 viewportFraction: viewportFraction ?? this.viewportFraction,
494 devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
495 );
496 }
497}
498
499class _ForceImplicitScrollPhysics extends ScrollPhysics {
500 const _ForceImplicitScrollPhysics({
501 required this.allowImplicitScrolling,
502 super.parent,
503 });
504
505 @override
506 _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
507 return _ForceImplicitScrollPhysics(
508 allowImplicitScrolling: allowImplicitScrolling,
509 parent: buildParent(ancestor),
510 );
511 }
512
513 @override
514 final bool allowImplicitScrolling;
515}
516
517/// Scroll physics used by a [PageView].
518///
519/// These physics cause the page view to snap to page boundaries.
520///
521/// See also:
522///
523/// * [ScrollPhysics], the base class which defines the API for scrolling
524/// physics.
525/// * [PageView.physics], which can override the physics used by a page view.
526class PageScrollPhysics extends ScrollPhysics {
527 /// Creates physics for a [PageView].
528 const PageScrollPhysics({ super.parent });
529
530 @override
531 PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
532 return PageScrollPhysics(parent: buildParent(ancestor));
533 }
534
535 double _getPage(ScrollMetrics position) {
536 if (position is _PagePosition) {
537 return position.page!;
538 }
539 return position.pixels / position.viewportDimension;
540 }
541
542 double _getPixels(ScrollMetrics position, double page) {
543 if (position is _PagePosition) {
544 return position.getPixelsFromPage(page);
545 }
546 return page * position.viewportDimension;
547 }
548
549 double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
550 double page = _getPage(position);
551 if (velocity < -tolerance.velocity) {
552 page -= 0.5;
553 } else if (velocity > tolerance.velocity) {
554 page += 0.5;
555 }
556 return _getPixels(position, page.roundToDouble());
557 }
558
559 @override
560 Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
561 // If we're out of range and not headed back in range, defer to the parent
562 // ballistics, which should put us back in range at a page boundary.
563 if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
564 (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
565 return super.createBallisticSimulation(position, velocity);
566 }
567 final Tolerance tolerance = toleranceFor(position);
568 final double target = _getTargetPixels(position, tolerance, velocity);
569 if (target != position.pixels) {
570 return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
571 }
572 return null;
573 }
574
575 @override
576 bool get allowImplicitScrolling => false;
577}
578
579// Having this global (mutable) page controller is a bit of a hack. We need it
580// to plumb in the factory for _PagePosition, but it will end up accumulating
581// a large list of scroll positions. As long as you don't try to actually
582// control the scroll positions, everything should be fine.
583final PageController _defaultPageController = PageController();
584const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
585
586/// A scrollable list that works page by page.
587///
588/// Each child of a page view is forced to be the same size as the viewport.
589///
590/// You can use a [PageController] to control which page is visible in the view.
591/// In addition to being able to control the pixel offset of the content inside
592/// the [PageView], a [PageController] also lets you control the offset in terms
593/// of pages, which are increments of the viewport size.
594///
595/// The [PageController] can also be used to control the
596/// [PageController.initialPage], which determines which page is shown when the
597/// [PageView] is first constructed, and the [PageController.viewportFraction],
598/// which determines the size of the pages as a fraction of the viewport size.
599///
600/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
601///
602/// {@tool dartpad}
603/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
604/// which scroll horizontally.
605///
606/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
607/// {@end-tool}
608///
609/// ## Persisting the scroll position during a session
610///
611/// Scroll views attempt to persist their scroll position using [PageStorage].
612/// For a [PageView], this can be disabled by setting [PageController.keepPage]
613/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
614/// the [key] of this widget is recommended to help disambiguate different
615/// scroll views from each other.
616///
617/// See also:
618///
619/// * [PageController], which controls which page is visible in the view.
620/// * [SingleChildScrollView], when you need to make a single child scrollable.
621/// * [ListView], for a scrollable list of boxes.
622/// * [GridView], for a scrollable grid of boxes.
623/// * [ScrollNotification] and [NotificationListener], which can be used to watch
624/// the scroll position without using a [ScrollController].
625class PageView extends StatefulWidget {
626 /// Creates a scrollable list that works page by page from an explicit [List]
627 /// of widgets.
628 ///
629 /// This constructor is appropriate for page views with a small number of
630 /// children because constructing the [List] requires doing work for every
631 /// child that could possibly be displayed in the page view, instead of just
632 /// those children that are actually visible.
633 ///
634 /// Like other widgets in the framework, this widget expects that
635 /// the [children] list will not be mutated after it has been passed in here.
636 /// See the documentation at [SliverChildListDelegate.children] for more details.
637 ///
638 /// {@template flutter.widgets.PageView.allowImplicitScrolling}
639 /// If [allowImplicitScrolling] is true, the [PageView] will participate in
640 /// accessibility scrolling more like a [ListView], where implicit scroll
641 /// actions will move to the next page rather than into the contents of the
642 /// [PageView].
643 /// {@endtemplate}
644 PageView({
645 super.key,
646 this.scrollDirection = Axis.horizontal,
647 this.reverse = false,
648 PageController? controller,
649 this.physics,
650 this.pageSnapping = true,
651 this.onPageChanged,
652 List<Widget> children = const <Widget>[],
653 this.dragStartBehavior = DragStartBehavior.start,
654 this.allowImplicitScrolling = false,
655 this.restorationId,
656 this.clipBehavior = Clip.hardEdge,
657 this.scrollBehavior,
658 this.padEnds = true,
659 }) : controller = controller ?? _defaultPageController,
660 childrenDelegate = SliverChildListDelegate(children);
661
662 /// Creates a scrollable list that works page by page using widgets that are
663 /// created on demand.
664 ///
665 /// This constructor is appropriate for page views with a large (or infinite)
666 /// number of children because the builder is called only for those children
667 /// that are actually visible.
668 ///
669 /// Providing a non-null [itemCount] lets the [PageView] compute the maximum
670 /// scroll extent.
671 ///
672 /// [itemBuilder] will be called only with indices greater than or equal to
673 /// zero and less than [itemCount].
674 ///
675 /// {@macro flutter.widgets.ListView.builder.itemBuilder}
676 ///
677 /// {@template flutter.widgets.PageView.findChildIndexCallback}
678 /// The [findChildIndexCallback] corresponds to the
679 /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
680 /// a child widget may not map to its existing [RenderObject] when the order
681 /// of children returned from the children builder changes.
682 /// This may result in state-loss. This callback needs to be implemented if
683 /// the order of the children may change at a later time.
684 /// {@endtemplate}
685 ///
686 /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
687 PageView.builder({
688 super.key,
689 this.scrollDirection = Axis.horizontal,
690 this.reverse = false,
691 PageController? controller,
692 this.physics,
693 this.pageSnapping = true,
694 this.onPageChanged,
695 required NullableIndexedWidgetBuilder itemBuilder,
696 ChildIndexGetter? findChildIndexCallback,
697 int? itemCount,
698 this.dragStartBehavior = DragStartBehavior.start,
699 this.allowImplicitScrolling = false,
700 this.restorationId,
701 this.clipBehavior = Clip.hardEdge,
702 this.scrollBehavior,
703 this.padEnds = true,
704 }) : controller = controller ?? _defaultPageController,
705 childrenDelegate = SliverChildBuilderDelegate(
706 itemBuilder,
707 findChildIndexCallback: findChildIndexCallback,
708 childCount: itemCount,
709 );
710
711 /// Creates a scrollable list that works page by page with a custom child
712 /// model.
713 ///
714 /// {@tool dartpad}
715 /// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child
716 /// reordering.
717 ///
718 /// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
719 /// {@end-tool}
720 ///
721 /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
722 PageView.custom({
723 super.key,
724 this.scrollDirection = Axis.horizontal,
725 this.reverse = false,
726 PageController? controller,
727 this.physics,
728 this.pageSnapping = true,
729 this.onPageChanged,
730 required this.childrenDelegate,
731 this.dragStartBehavior = DragStartBehavior.start,
732 this.allowImplicitScrolling = false,
733 this.restorationId,
734 this.clipBehavior = Clip.hardEdge,
735 this.scrollBehavior,
736 this.padEnds = true,
737 }) : controller = controller ?? _defaultPageController;
738
739 /// Controls whether the widget's pages will respond to
740 /// [RenderObject.showOnScreen], which will allow for implicit accessibility
741 /// scrolling.
742 ///
743 /// With this flag set to false, when accessibility focus reaches the end of
744 /// the current page and the user attempts to move it to the next element, the
745 /// focus will traverse to the next widget outside of the page view.
746 ///
747 /// With this flag set to true, when accessibility focus reaches the end of
748 /// the current page and user attempts to move it to the next element, focus
749 /// will traverse to the next page in the page view.
750 final bool allowImplicitScrolling;
751
752 /// {@macro flutter.widgets.scrollable.restorationId}
753 final String? restorationId;
754
755 /// The [Axis] along which the scroll view's offset increases with each page.
756 ///
757 /// For the direction in which active scrolling may be occurring, see
758 /// [ScrollDirection].
759 ///
760 /// Defaults to [Axis.horizontal].
761 final Axis scrollDirection;
762
763 /// Whether the page view scrolls in the reading direction.
764 ///
765 /// For example, if the reading direction is left-to-right and
766 /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
767 /// left to right when [reverse] is false and from right to left when
768 /// [reverse] is true.
769 ///
770 /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
771 /// scrolls from top to bottom when [reverse] is false and from bottom to top
772 /// when [reverse] is true.
773 ///
774 /// Defaults to false.
775 final bool reverse;
776
777 /// An object that can be used to control the position to which this page
778 /// view is scrolled.
779 final PageController controller;
780
781 /// How the page view should respond to user input.
782 ///
783 /// For example, determines how the page view continues to animate after the
784 /// user stops dragging the page view.
785 ///
786 /// The physics are modified to snap to page boundaries using
787 /// [PageScrollPhysics] prior to being used.
788 ///
789 /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
790 /// [ScrollPhysics] provided by that behavior will take precedence after
791 /// [physics].
792 ///
793 /// Defaults to matching platform conventions.
794 final ScrollPhysics? physics;
795
796 /// Set to false to disable page snapping, useful for custom scroll behavior.
797 ///
798 /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
799 /// the page will snap to the beginning of the viewport; otherwise, the page
800 /// will snap to the center of the viewport.
801 final bool pageSnapping;
802
803 /// Called whenever the page in the center of the viewport changes.
804 final ValueChanged<int>? onPageChanged;
805
806 /// A delegate that provides the children for the [PageView].
807 ///
808 /// The [PageView.custom] constructor lets you specify this delegate
809 /// explicitly. The [PageView] and [PageView.builder] constructors create a
810 /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
811 /// respectively.
812 final SliverChildDelegate childrenDelegate;
813
814 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
815 final DragStartBehavior dragStartBehavior;
816
817 /// {@macro flutter.material.Material.clipBehavior}
818 ///
819 /// Defaults to [Clip.hardEdge].
820 final Clip clipBehavior;
821
822 /// {@macro flutter.widgets.shadow.scrollBehavior}
823 ///
824 /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
825 /// [ScrollPhysics] is provided in [physics], it will take precedence,
826 /// followed by [scrollBehavior], and then the inherited ancestor
827 /// [ScrollBehavior].
828 ///
829 /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
830 /// modified by default to not apply a [Scrollbar].
831 final ScrollBehavior? scrollBehavior;
832
833 /// Whether to add padding to both ends of the list.
834 ///
835 /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
836 /// such that the first and last child slivers will be in the center of
837 /// the viewport when scrolled all the way to the start or end, respectively.
838 ///
839 /// If [PageController.viewportFraction] >= 1.0, this property has no effect.
840 ///
841 /// This property defaults to true.
842 final bool padEnds;
843
844 @override
845 State<PageView> createState() => _PageViewState();
846}
847
848class _PageViewState extends State<PageView> {
849 int _lastReportedPage = 0;
850
851 @override
852 void initState() {
853 super.initState();
854 _lastReportedPage = widget.controller.initialPage;
855 }
856
857 AxisDirection _getDirection(BuildContext context) {
858 switch (widget.scrollDirection) {
859 case Axis.horizontal:
860 assert(debugCheckHasDirectionality(context));
861 final TextDirection textDirection = Directionality.of(context);
862 final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
863 return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
864 case Axis.vertical:
865 return widget.reverse ? AxisDirection.up : AxisDirection.down;
866 }
867 }
868
869 @override
870 Widget build(BuildContext context) {
871 final AxisDirection axisDirection = _getDirection(context);
872 final ScrollPhysics physics = _ForceImplicitScrollPhysics(
873 allowImplicitScrolling: widget.allowImplicitScrolling,
874 ).applyTo(
875 widget.pageSnapping
876 ? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
877 : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
878 );
879
880 return NotificationListener<ScrollNotification>(
881 onNotification: (ScrollNotification notification) {
882 if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
883 final PageMetrics metrics = notification.metrics as PageMetrics;
884 final int currentPage = metrics.page!.round();
885 if (currentPage != _lastReportedPage) {
886 _lastReportedPage = currentPage;
887 widget.onPageChanged!(currentPage);
888 }
889 }
890 return false;
891 },
892 child: Scrollable(
893 dragStartBehavior: widget.dragStartBehavior,
894 axisDirection: axisDirection,
895 controller: widget.controller,
896 physics: physics,
897 restorationId: widget.restorationId,
898 scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
899 viewportBuilder: (BuildContext context, ViewportOffset position) {
900 return Viewport(
901 // TODO(dnfield): we should provide a way to set cacheExtent
902 // independent of implicit scrolling:
903 // https://github.com/flutter/flutter/issues/45632
904 cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
905 cacheExtentStyle: CacheExtentStyle.viewport,
906 axisDirection: axisDirection,
907 offset: position,
908 clipBehavior: widget.clipBehavior,
909 slivers: <Widget>[
910 SliverFillViewport(
911 viewportFraction: widget.controller.viewportFraction,
912 delegate: widget.childrenDelegate,
913 padEnds: widget.padEnds,
914 ),
915 ],
916 );
917 },
918 ),
919 );
920 }
921
922 @override
923 void debugFillProperties(DiagnosticPropertiesBuilder description) {
924 super.debugFillProperties(description);
925 description.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
926 description.add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'));
927 description.add(DiagnosticsProperty<PageController>('controller', widget.controller, showName: false));
928 description.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics, showName: false));
929 description.add(FlagProperty('pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled'));
930 description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
931 }
932}
933