blob: c80460790298e61fe6d0b2d0892599df21593fac [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show precisionErrorTolerance;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'scroll_view.dart';
import 'scrollable.dart';
import 'sliver.dart';
import 'sliver_fill.dart';
import 'viewport.dart';
/// A controller for [PageView].
///
/// A page controller lets you manipulate which page is visible in a [PageView].
/// In addition to being able to control the pixel offset of the content inside
/// the [PageView], a [PageController] also lets you control the offset in terms
/// of pages, which are increments of the viewport size.
///
/// See also:
///
/// * [PageView], which is the widget this object controls.
///
/// {@tool snippet}
///
/// This widget introduces a [MaterialApp], [Scaffold] and [PageView] with two pages
/// using the default constructor. Both pages contain an [ElevatedButton] allowing you
/// to animate the [PageView] using a [PageController].
///
/// ```dart
/// class MyPageView extends StatefulWidget {
/// const MyPageView({Key? key}) : super(key: key);
///
/// @override
/// State<MyPageView> createState() => _MyPageViewState();
/// }
///
/// class _MyPageViewState extends State<MyPageView> {
/// final PageController _pageController = PageController();
///
/// @override
/// void dispose() {
/// _pageController.dispose();
/// super.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Scaffold(
/// body: PageView(
/// controller: _pageController,
/// children: <Widget>[
/// Container(
/// color: Colors.red,
/// child: Center(
/// child: ElevatedButton(
/// onPressed: () {
/// if (_pageController.hasClients) {
/// _pageController.animateToPage(
/// 1,
/// duration: const Duration(milliseconds: 400),
/// curve: Curves.easeInOut,
/// );
/// }
/// },
/// child: const Text('Next'),
/// ),
/// ),
/// ),
/// Container(
/// color: Colors.blue,
/// child: Center(
/// child: ElevatedButton(
/// onPressed: () {
/// if (_pageController.hasClients) {
/// _pageController.animateToPage(
/// 0,
/// duration: const Duration(milliseconds: 400),
/// curve: Curves.easeInOut,
/// );
/// }
/// },
/// child: const Text('Previous'),
/// ),
/// ),
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
///
/// ```
/// {@end-tool}
class PageController extends ScrollController {
/// Creates a page controller.
///
/// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null.
PageController({
this.initialPage = 0,
this.keepPage = true,
this.viewportFraction = 1.0,
}) : assert(initialPage != null),
assert(keepPage != null),
assert(viewportFraction != null),
assert(viewportFraction > 0.0);
/// The page to show when first creating the [PageView].
final int initialPage;
/// Save the current [page] with [PageStorage] and restore it if
/// this controller's scrollable is recreated.
///
/// If this property is set to false, the current [page] is never saved
/// and [initialPage] is always used to initialize the scroll offset.
/// If true (the default), the initial page is used the first time the
/// controller's scrollable is created, since there's isn't a page to
/// restore yet. Subsequently the saved page is restored and
/// [initialPage] is ignored.
///
/// See also:
///
/// * [PageStorageKey], which should be used when more than one
/// scrollable appears in the same route, to distinguish the [PageStorage]
/// locations used to save scroll offsets.
final bool keepPage;
/// The fraction of the viewport that each page should occupy.
///
/// Defaults to 1.0, which means each page fills the viewport in the scrolling
/// direction.
final double viewportFraction;
/// The current page displayed in the controlled [PageView].
///
/// There are circumstances that this [PageController] can't know the current
/// page. Reading [page] will throw an [AssertionError] in the following cases:
///
/// 1. No [PageView] is currently using this [PageController]. Once a
/// [PageView] starts using this [PageController], the new [page]
/// position will be derived:
///
/// * First, based on the attached [PageView]'s [BuildContext] and the
/// position saved at that context's [PageStorage] if [keepPage] is true.
/// * Second, from the [PageController]'s [initialPage].
///
/// 2. More than one [PageView] using the same [PageController].
///
/// The [hasClients] property can be used to check if a [PageView] is attached
/// prior to accessing [page].
double? get page {
assert(
positions.isNotEmpty,
'PageController.page cannot be accessed before a PageView is built with it.',
);
assert(
positions.length == 1,
'The page property cannot be read when multiple PageViews are attached to '
'the same PageController.',
);
final _PagePosition position = this.position as _PagePosition;
return position.page;
}
/// Animates the controlled [PageView] from the current page to the given page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
///
/// The `duration` and `curve` arguments must not be null.
Future<void> animateToPage(
int page, {
required Duration duration,
required Curve curve,
}) {
final _PagePosition position = this.position as _PagePosition;
if (position._cachedPage != null) {
position._cachedPage = page.toDouble();
return Future<void>.value();
}
return position.animateTo(
position.getPixelsFromPage(page.toDouble()),
duration: duration,
curve: curve,
);
}
/// Changes which page is displayed in the controlled [PageView].
///
/// Jumps the page position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
void jumpToPage(int page) {
final _PagePosition position = this.position as _PagePosition;
if (position._cachedPage != null) {
position._cachedPage = page.toDouble();
return;
}
position.jumpTo(position.getPixelsFromPage(page.toDouble()));
}
/// Animates the controlled [PageView] to the next page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
///
/// The `duration` and `curve` arguments must not be null.
Future<void> nextPage({ required Duration duration, required Curve curve }) {
return animateToPage(page!.round() + 1, duration: duration, curve: curve);
}
/// Animates the controlled [PageView] to the previous page.
///
/// The animation lasts for the given duration and follows the given curve.
/// The returned [Future] resolves when the animation completes.
///
/// The `duration` and `curve` arguments must not be null.
Future<void> previousPage({ required Duration duration, required Curve curve }) {
return animateToPage(page!.round() - 1, duration: duration, curve: curve);
}
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {
return _PagePosition(
physics: physics,
context: context,
initialPage: initialPage,
keepPage: keepPage,
viewportFraction: viewportFraction,
oldPosition: oldPosition,
);
}
@override
void attach(ScrollPosition position) {
super.attach(position);
final _PagePosition pagePosition = position as _PagePosition;
pagePosition.viewportFraction = viewportFraction;
}
}
/// Metrics for a [PageView].
///
/// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s.
class PageMetrics extends FixedScrollMetrics {
/// Creates an immutable snapshot of values associated with a [PageView].
PageMetrics({
required double? minScrollExtent,
required double? maxScrollExtent,
required double? pixels,
required double? viewportDimension,
required AxisDirection axisDirection,
required this.viewportFraction,
}) : super(
minScrollExtent: minScrollExtent,
maxScrollExtent: maxScrollExtent,
pixels: pixels,
viewportDimension: viewportDimension,
axisDirection: axisDirection,
);
@override
PageMetrics copyWith({
double? minScrollExtent,
double? maxScrollExtent,
double? pixels,
double? viewportDimension,
AxisDirection? axisDirection,
double? viewportFraction,
}) {
return PageMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
pixels: pixels ?? (hasPixels ? this.pixels : null),
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
viewportFraction: viewportFraction ?? this.viewportFraction,
);
}
/// The current page displayed in the [PageView].
double? get page {
return math.max(0.0, pixels.clamp(minScrollExtent, maxScrollExtent)) /
math.max(1.0, viewportDimension * viewportFraction);
}
/// The fraction of the viewport that each page occupies.
///
/// Used to compute [page] from the current [pixels].
final double viewportFraction;
}
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
_PagePosition({
required ScrollPhysics physics,
required ScrollContext context,
this.initialPage = 0,
bool keepPage = true,
double viewportFraction = 1.0,
ScrollPosition? oldPosition,
}) : assert(initialPage != null),
assert(keepPage != null),
assert(viewportFraction != null),
assert(viewportFraction > 0.0),
_viewportFraction = viewportFraction,
_pageToUseOnStartup = initialPage.toDouble(),
super(
physics: physics,
context: context,
initialPixels: null,
keepScrollOffset: keepPage,
oldPosition: oldPosition,
);
final int initialPage;
double _pageToUseOnStartup;
// When the viewport has a zero-size, the `page` can not
// be retrieved by `getPageFromPixels`, so we need to cache the page
// for use when resizing the viewport to non-zero next time.
double? _cachedPage;
@override
Future<void> ensureVisible(
RenderObject object, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
// Since the _PagePosition is intended to cover the available space within
// its viewport, stop trying to move the target render object to the center
// - otherwise, could end up changing which page is visible and moving the
// targetRenderObject out of the viewport.
return super.ensureVisible(
object,
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
);
}
@override
double get viewportFraction => _viewportFraction;
double _viewportFraction;
set viewportFraction(double value) {
if (_viewportFraction == value)
return;
final double? oldPage = page;
_viewportFraction = value;
if (oldPage != null)
forcePixels(getPixelsFromPage(oldPage));
}
// The amount of offset that will be added to [minScrollExtent] and subtracted
// from [maxScrollExtent], such that every page will properly snap to the center
// of the viewport when viewportFraction is greater than 1.
//
// The value is 0 if viewportFraction is less than or equal to 1, larger than 0
// otherwise.
double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2);
double getPageFromPixels(double pixels, double viewportDimension) {
assert(viewportDimension > 0.0);
final double actual = math.max(0.0, pixels - _initialPageOffset) / (viewportDimension * viewportFraction);
final double round = actual.roundToDouble();
if ((actual - round).abs() < precisionErrorTolerance) {
return round;
}
return actual;
}
double getPixelsFromPage(double page) {
return page * viewportDimension * viewportFraction + _initialPageOffset;
}
@override
double? get page {
assert(
!hasPixels || hasContentDimensions,
'Page value is only available after content dimensions are established.',
);
return !hasPixels || !hasContentDimensions
? null
: _cachedPage ?? getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
}
@override
void saveScrollOffset() {
PageStorage.of(context.storageContext)?.writeState(context.storageContext, _cachedPage ?? getPageFromPixels(pixels, viewportDimension));
}
@override
void restoreScrollOffset() {
if (!hasPixels) {
final double? value = PageStorage.of(context.storageContext)?.readState(context.storageContext) as double?;
if (value != null)
_pageToUseOnStartup = value;
}
}
@override
void saveOffset() {
context.saveOffset(_cachedPage ?? getPageFromPixels(pixels, viewportDimension));
}
@override
void restoreOffset(double offset, {bool initialRestore = false}) {
assert(initialRestore != null);
assert(offset != null);
if (initialRestore) {
_pageToUseOnStartup = offset;
} else {
jumpTo(getPixelsFromPage(offset));
}
}
@override
bool applyViewportDimension(double viewportDimension) {
final double? oldViewportDimensions = hasViewportDimension ? this.viewportDimension : null;
if (viewportDimension == oldViewportDimensions) {
return true;
}
final bool result = super.applyViewportDimension(viewportDimension);
final double? oldPixels = hasPixels ? pixels : null;
double page;
if (oldPixels == null) {
page = _pageToUseOnStartup;
} else if (oldViewportDimensions == 0.0) {
// If resize from zero, we should use the _cachedPage to recover the state.
page = _cachedPage!;
} else {
page = getPageFromPixels(oldPixels, oldViewportDimensions!);
}
final double newPixels = getPixelsFromPage(page);
// If the viewportDimension is zero, cache the page
// in case the viewport is resized to be non-zero.
_cachedPage = (viewportDimension == 0.0) ? page : null;
if (newPixels != oldPixels) {
correctPixels(newPixels);
return false;
}
return result;
}
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
return super.applyContentDimensions(
newMinScrollExtent,
math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
);
}
@override
PageMetrics copyWith({
double? minScrollExtent,
double? maxScrollExtent,
double? pixels,
double? viewportDimension,
AxisDirection? axisDirection,
double? viewportFraction,
}) {
return PageMetrics(
minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
pixels: pixels ?? (hasPixels ? this.pixels : null),
viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
axisDirection: axisDirection ?? this.axisDirection,
viewportFraction: viewportFraction ?? this.viewportFraction,
);
}
}
class _ForceImplicitScrollPhysics extends ScrollPhysics {
const _ForceImplicitScrollPhysics({
required this.allowImplicitScrolling,
ScrollPhysics? parent,
}) : assert(allowImplicitScrolling != null),
super(parent: parent);
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
return _ForceImplicitScrollPhysics(
allowImplicitScrolling: allowImplicitScrolling,
parent: buildParent(ancestor),
);
}
@override
final bool allowImplicitScrolling;
}
/// Scroll physics used by a [PageView].
///
/// These physics cause the page view to snap to page boundaries.
///
/// See also:
///
/// * [ScrollPhysics], the base class which defines the API for scrolling
/// physics.
/// * [PageView.physics], which can override the physics used by a page view.
class PageScrollPhysics extends ScrollPhysics {
/// Creates physics for a [PageView].
const PageScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);
@override
PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PageScrollPhysics(parent: buildParent(ancestor));
}
double _getPage(ScrollMetrics position) {
if (position is _PagePosition)
return position.page!;
return position.pixels / position.viewportDimension;
}
double _getPixels(ScrollMetrics position, double page) {
if (position is _PagePosition)
return position.getPixelsFromPage(page);
return page * position.viewportDimension;
}
double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
if (velocity < -tolerance.velocity)
page -= 0.5;
else if (velocity > tolerance.velocity)
page += 0.5;
return _getPixels(position, page.roundToDouble());
}
@override
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
// Having this global (mutable) page controller is a bit of a hack. We need it
// to plumb in the factory for _PagePosition, but it will end up accumulating
// a large list of scroll positions. As long as you don't try to actually
// control the scroll positions, everything should be fine.
final PageController _defaultPageController = PageController();
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// A scrollable list that works page by page.
///
/// Each child of a page view is forced to be the same size as the viewport.
///
/// You can use a [PageController] to control which page is visible in the view.
/// In addition to being able to control the pixel offset of the content inside
/// the [PageView], a [PageController] also lets you control the offset in terms
/// of pages, which are increments of the viewport size.
///
/// The [PageController] can also be used to control the
/// [PageController.initialPage], which determines which page is shown when the
/// [PageView] is first constructed, and the [PageController.viewportFraction],
/// which determines the size of the pages as a fraction of the viewport size.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
///
/// {@tool dartpad}
/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally.
///
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [PageController], which controls which page is visible in the view.
/// * [SingleChildScrollView], when you need to make a single child scrollable.
/// * [ListView], for a scrollable list of boxes.
/// * [GridView], for a scrollable grid of boxes.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class PageView extends StatefulWidget {
/// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets.
///
/// This constructor is appropriate for page views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the page view, instead of just
/// those children that are actually visible.
///
/// Like other widgets in the framework, this widget expects that
/// the [children] list will not be mutated after it has been passed in here.
/// See the documentation at [SliverChildListDelegate.children] for more details.
///
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
/// The [allowImplicitScrolling] parameter must not be null. If true, the
/// [PageView] will participate in accessibility scrolling more like a
/// [ListView], where implicit scroll actions will move to the next page
/// rather than into the contents of the [PageView].
/// {@endtemplate}
PageView({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
}) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildListDelegate(children),
super(key: key);
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
///
/// This constructor is appropriate for page views with a large (or infinite)
/// number of children because the builder is called only for those children
/// that are actually visible.
///
/// Providing a non-null [itemCount] lets the [PageView] compute the maximum
/// scroll extent.
///
/// [itemBuilder] will be called only with indices greater than or equal to
/// zero and less than [itemCount].
///
/// [PageView.builder] by default does not support child reordering. If
/// you are planning to change child order at a later time, consider using
/// [PageView] or [PageView.custom].
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
PageView.builder({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required IndexedWidgetBuilder itemBuilder,
int? itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
}) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
super(key: key);
/// Creates a scrollable list that works page by page with a custom child
/// model.
///
/// {@tool snippet}
///
/// This [PageView] uses a custom [SliverChildBuilderDelegate] to support child
/// reordering.
///
/// ```dart
/// class MyPageView extends StatefulWidget {
/// const MyPageView({Key? key}) : super(key: key);
///
/// @override
/// State<MyPageView> createState() => _MyPageViewState();
/// }
///
/// class _MyPageViewState extends State<MyPageView> {
/// List<String> items = <String>['1', '2', '3', '4', '5'];
///
/// void _reverse() {
/// setState(() {
/// items = items.reversed.toList();
/// });
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: SafeArea(
/// child: PageView.custom(
/// childrenDelegate: SliverChildBuilderDelegate(
/// (BuildContext context, int index) {
/// return KeepAlive(
/// data: items[index],
/// key: ValueKey<String>(items[index]),
/// );
/// },
/// childCount: items.length,
/// findChildIndexCallback: (Key key) {
/// final ValueKey<String> valueKey = key as ValueKey<String>;
/// final String data = valueKey.value;
/// return items.indexOf(data);
/// }
/// ),
/// ),
/// ),
/// bottomNavigationBar: BottomAppBar(
/// child: Row(
/// mainAxisAlignment: MainAxisAlignment.center,
/// children: <Widget>[
/// TextButton(
/// onPressed: () => _reverse(),
/// child: const Text('Reverse items'),
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
///
/// class KeepAlive extends StatefulWidget {
/// const KeepAlive({Key? key, required this.data}) : super(key: key);
///
/// final String data;
///
/// @override
/// State<KeepAlive> createState() => _KeepAliveState();
/// }
///
/// class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin{
/// @override
/// bool get wantKeepAlive => true;
///
/// @override
/// Widget build(BuildContext context) {
/// super.build(context);
/// return Text(widget.data);
/// }
/// }
/// ```
/// {@end-tool}
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
PageView.custom({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
}) : assert(childrenDelegate != null),
assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
controller = controller ?? _defaultPageController,
super(key: key);
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
///
/// With this flag set to false, when accessibility focus reaches the end of
/// the current page and the user attempts to move it to the next element, the
/// focus will traverse to the next widget outside of the page view.
///
/// With this flag set to true, when accessibility focus reaches the end of
/// the current page and user attempts to move it to the next element, focus
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Whether the page view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this page
/// view is scrolled.
final PageController controller;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
/// the page will snap to the beginning of the viewport; otherwise, the page
/// will snap to the center of the viewport.
final bool pageSnapping;
/// Called whenever the page in the center of the viewport changes.
final ValueChanged<int>? onPageChanged;
/// A delegate that provides the children for the [PageView].
///
/// The [PageView.custom] constructor lets you specify this delegate
/// explicitly. The [PageView] and [PageView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively.
final SliverChildDelegate childrenDelegate;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
/// Whether to add padding to both ends of the list.
///
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of
/// the viewport when scrolled all the way to the start or end, respectively.
///
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
///
/// This property defaults to true and must not be null.
final bool padEnds;
@override
State<PageView> createState() => _PageViewState();
}
class _PageViewState extends State<PageView> {
int _lastReportedPage = 0;
@override
void initState() {
super.initState();
_lastReportedPage = widget.controller.initialPage;
}
AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection;
case Axis.vertical:
return widget.reverse ? AxisDirection.up : AxisDirection.down;
}
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
description.add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'));
description.add(DiagnosticsProperty<PageController>('controller', widget.controller, showName: false));
description.add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics, showName: false));
description.add(FlagProperty('pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled'));
description.add(FlagProperty('allowImplicitScrolling', value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling'));
}
}