mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-28 22:39:21 +00:00
1009 lines
35 KiB
Dart
1009 lines
35 KiB
Dart
|
// 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<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(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<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 [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<void> 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<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 [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<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}) {
|
||
|
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<Widget> children = const <Widget>[],
|
||
|
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<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}
|
||
|
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<int>? 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<CustomPageView> createState() => _CustomPageViewState();
|
||
|
}
|
||
|
|
||
|
class _CustomPageViewState extends State<CustomPageView> {
|
||
|
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>[
|
||
|
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<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'));
|
||
|
}
|
||
|
}
|