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 | 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. |
286 | class 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 | |
332 | class _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 | |
540 | class _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. |
564 | class 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 | |
623 | const 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]. |
664 | class 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 | |
888 | class _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 |
Definitions
- PageController
- PageController
- page
- _debugCheckPageControllerAttached
- 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