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'; |
9 | library; |
10 | |
11 | import 'dart:math' as math; |
12 | |
13 | import 'package:flutter/foundation.dart' show clampDouble, precisionErrorTolerance; |
14 | import 'package:flutter/gestures.dart' show DragStartBehavior; |
15 | import 'package:flutter/rendering.dart'; |
16 | |
17 | import 'basic.dart'; |
18 | import 'debug.dart'; |
19 | import 'framework.dart'; |
20 | import 'notification_listener.dart'; |
21 | import 'page_storage.dart'; |
22 | import 'scroll_configuration.dart'; |
23 | import 'scroll_context.dart'; |
24 | import 'scroll_controller.dart'; |
25 | import 'scroll_delegate.dart'; |
26 | import 'scroll_metrics.dart'; |
27 | import 'scroll_notification.dart'; |
28 | import 'scroll_physics.dart'; |
29 | import 'scroll_position.dart'; |
30 | import 'scroll_position_with_single_context.dart'; |
31 | import 'scroll_view.dart'; |
32 | import 'scrollable.dart'; |
33 | import 'sliver_fill.dart'; |
34 | import '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} |
119 | class 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. |
274 | class 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 | |
319 | class _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 | |
518 | class _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. |
545 | class 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 | |
598 | const 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]. |
639 | class 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 | |
868 | class _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 |
Definitions
- PageController
- PageController
- page
- animateToPage
- jumpToPage
- nextPage
- previousPage
- createScrollPosition
- attach
- PageMetrics
- PageMetrics
- copyWith
- page
- _PagePosition
- _PagePosition
- ensureVisible
- viewportFraction
- viewportFraction
- _initialPageOffset
- getPageFromPixels
- getPixelsFromPage
- page
- saveScrollOffset
- restoreScrollOffset
- saveOffset
- restoreOffset
- applyViewportDimension
- absorb
- applyContentDimensions
- copyWith
- _ForceImplicitScrollPhysics
- _ForceImplicitScrollPhysics
- applyTo
- PageScrollPhysics
- PageScrollPhysics
- applyTo
- _getPage
- _getPixels
- _getTargetPixels
- createBallisticSimulation
- allowImplicitScrolling
- _kPagePhysics
- PageView
- PageView
- builder
- custom
- createState
- _PageViewState
- initState
- dispose
- _initController
- didUpdateWidget
- _getDirection
- build
Learn more about Flutter for embedded and desktop on industrialflutter.com