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

Provided by KDAB

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