// 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/cupertino.dart'; import 'package:flutter/foundation.dart' show precisionErrorTolerance; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/rendering.dart'; import 'package:stackwallet/widgets/custom_page_view/custom_sliver_fill_viewport.dart'; /// A controller for [CustomPageView]. /// /// A page controller lets you manipulate which page is visible in a [CustomPageView]. /// In addition to being able to control the pixel offset of the content inside /// the [CustomPageView], a [PageController] also lets you control the offset in terms /// of pages, which are increments of the viewport size. /// /// See also: /// /// * [CustomPageView], which is the widget this object controls. /// /// {@tool snippet} /// /// This widget introduces a [MaterialApp], [Scaffold] and [CustomPageView] with two pages /// using the default constructor. Both pages contain an [ElevatedButton] allowing you /// to animate the [CustomPageView] using a [PageController]. /// /// ```dart /// class MyPageView extends StatefulWidget { /// const MyPageView({Key? key}) : super(key: key); /// /// @override /// State createState() => _MyPageViewState(); /// } /// /// class _MyPageViewState extends State { /// 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: [ /// 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(viewportFraction > 0.0); /// The page to show when first creating the [CustomPageView]. 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; /// {@template flutter.widgets.pageview.viewportFraction} /// 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. /// {@endtemplate} final double viewportFraction; /// The current page displayed in the controlled [CustomPageView]. /// /// There are circumstances that this [PageController] can't know the current /// page. Reading [page] will throw an [AssertionError] in the following cases: /// /// 1. No [CustomPageView] is currently using this [PageController]. Once a /// [CustomPageView] starts using this [PageController], the new [page] /// position will be derived: /// /// * First, based on the attached [CustomPageView]'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 [CustomPageView] using the same [PageController]. /// /// The [hasClients] property can be used to check if a [CustomPageView] 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 [CustomPageView] 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 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.value(); } return position.animateTo( position.getPixelsFromPage(page.toDouble()), duration: duration, curve: curve, ); } /// Changes which page is displayed in the controlled [CustomPageView]. /// /// 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 [CustomPageView] 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 nextPage({required Duration duration, required Curve curve}) { return animateToPage(page!.round() + 1, duration: duration, curve: curve); } /// Animates the controlled [CustomPageView] 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 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 [CustomPageView]. /// /// The metrics are available on [ScrollNotification]s generated from /// [CustomPageView]s. class PageMetrics extends FixedScrollMetrics { /// Creates an immutable snapshot of values associated with a [CustomPageView]. 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 [CustomPageView]. 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(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 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}) { 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, }) : super(parent: parent); @override _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { return _ForceImplicitScrollPhysics( allowImplicitScrolling: allowImplicitScrolling, parent: buildParent(ancestor), ); } @override final bool allowImplicitScrolling; } /// Scroll physics used by a [CustomPageView]. /// /// These physics cause the page view to snap to page boundaries. /// /// See also: /// /// * [ScrollPhysics], the base class which defines the API for scrolling /// physics. /// * [CustomPageView.physics], which can override the physics used by a page view. class PageScrollPhysics extends ScrollPhysics { /// Creates physics for a [CustomPageView]. 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 [CustomPageView], 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 /// [CustomPageView] 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 [CustomPageView]. 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 CustomPageView 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 /// [CustomPageView] 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 [CustomPageView]. /// {@endtemplate} CustomPageView({ Key? key, this.scrollDirection = Axis.horizontal, this.reverse = false, PageController? controller, this.physics, this.pageSnapping = true, this.onPageChanged, List children = const [], this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.viewportFractionalPadding = 0.25, this.scrollBehavior, this.padEnds = true, }) : 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 [CustomPageView] compute the maximum /// scroll extent. /// /// [itemBuilder] will be called only with indices greater than or equal to /// zero and less than [itemCount]. /// /// {@template flutter.widgets.PageView.findChildIndexCallback} /// The [findChildIndexCallback] corresponds to the /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null, /// a child widget may not map to its existing [RenderObject] when the order /// of children returned from the children builder changes. /// This may result in state-loss. This callback needs to be implemented if /// the order of the children may change at a later time. /// {@endtemplate} /// /// {@macro flutter.widgets.PageView.allowImplicitScrolling} CustomPageView.builder({ Key? key, this.scrollDirection = Axis.horizontal, this.reverse = false, PageController? controller, this.physics, this.pageSnapping = true, this.onPageChanged, required IndexedWidgetBuilder itemBuilder, ChildIndexGetter? findChildIndexCallback, int? itemCount, this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.viewportFractionalPadding = 0.25, this.scrollBehavior, this.padEnds = true, }) : controller = controller ?? _defaultPageController, childrenDelegate = SliverChildBuilderDelegate( itemBuilder, findChildIndexCallback: findChildIndexCallback, childCount: itemCount, ), super(key: key); /// Creates a scrollable list that works page by page with a custom child /// model. /// /// {@tool snippet} /// /// This [CustomPageView] uses a custom [SliverChildBuilderDelegate] to support child /// reordering. /// /// ```dart /// class MyPageView extends StatefulWidget { /// const MyPageView({Key? key}) : super(key: key); /// /// @override /// State createState() => _MyPageViewState(); /// } /// /// class _MyPageViewState extends State { /// List items = ['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(items[index]), /// ); /// }, /// childCount: items.length, /// findChildIndexCallback: (Key key) { /// final ValueKey valueKey = key as ValueKey; /// final String data = valueKey.value; /// return items.indexOf(data); /// } /// ), /// ), /// ), /// bottomNavigationBar: BottomAppBar( /// child: Row( /// mainAxisAlignment: MainAxisAlignment.center, /// children: [ /// 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 createState() => _KeepAliveState(); /// } /// /// class _KeepAliveState extends State 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} CustomPageView.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.viewportFractionalPadding = 0.25, this.padEnds = true, }) : controller = controller ?? _defaultPageController, super(key: key); final double viewportFractionalPadding; /// 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? onPageChanged; /// A delegate that provides the children for the [CustomPageView]. /// /// The [PageView.custom] constructor lets you specify this delegate /// explicitly. The [CustomPageView] 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 createState() => _CustomPageViewState(); } class _CustomPageViewState extends State { 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( 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: [ CustomSliverFillViewport( viewportFractionalPadding: widget.viewportFractionalPadding, viewportFraction: widget.controller.viewportFraction, delegate: widget.childrenDelegate, padEnds: widget.padEnds, ), ], ); }, ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description .add(EnumProperty('scrollDirection', widget.scrollDirection)); description.add( FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')); description.add(DiagnosticsProperty( 'controller', widget.controller, showName: false)); description.add(DiagnosticsProperty( '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')); } }