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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com