Created
November 25, 2025 11:43
-
-
Save urusaich/f7d1d194cce436aedaceaa7296d6c5d0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:collection'; | |
| import 'package:flutter/gestures.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| const smartPageSwitcherCurve = Curves.easeInOut; | |
| class SmartPageViewController { | |
| _SmartPageSwitcherImplState? _state; | |
| double get _height => _state!.widget.constraints.maxHeight; | |
| double get _width => _state!.widget.constraints.maxWidth; | |
| PageController get _pageController => _state!._controller; | |
| bool get isAnimating => _state!.ignorePointer; | |
| Future<void> go(int page) async { | |
| final currentPageIndex = _pageController.page!; | |
| final toRight = page > currentPageIndex; | |
| final toLeft = !toRight; | |
| if (_state!._ignorePointer || | |
| page == currentPageIndex || | |
| page > _state!.widget.children.length - 1) { | |
| return; | |
| } | |
| int rangeBegin; | |
| int rangeEnd; | |
| if (toRight) { | |
| rangeBegin = currentPageIndex.ceil() + 1; | |
| rangeEnd = page; | |
| } else { | |
| rangeBegin = page + 1; | |
| rangeEnd = currentPageIndex.ceil(); | |
| } | |
| final indexes = [..._state!.indexes]; | |
| final removedIndexes = <int>[]; | |
| for (var i = rangeEnd - 1; i >= rangeBegin; i--) { | |
| removedIndexes.add(indexes.removeAt(i)); | |
| } | |
| _state!.indexes = [...indexes, ...removedIndexes]; | |
| _state!.ignorePointer = true; | |
| try { | |
| if (toLeft) { | |
| _pageController.jumpToPage(rangeBegin); | |
| await _animateToPage(rangeBegin - 1); | |
| } else { | |
| await _animateToPage(rangeBegin); | |
| } | |
| } catch (e, s) { | |
| debugPrintStack(stackTrace: s, label: '$s'); | |
| } finally { | |
| _pageController.jumpToPage(page); | |
| _state!.ignorePointer = false; | |
| _state!._resetIndexes(); | |
| } | |
| } | |
| Future<void> _animateToPage(int page) => _pageController.animateToPage( | |
| page, | |
| duration: _state!.widget.duration, | |
| curve: _state!.widget.curve, | |
| ); | |
| } | |
| class SmartPageSwitcher extends StatelessWidget { | |
| const SmartPageSwitcher({ | |
| super.key, | |
| required this.controller, | |
| this.initialPage = 0, | |
| required this.children, | |
| required this.duration, | |
| this.curve = smartPageSwitcherCurve, | |
| this.onPageChanged, | |
| // page view | |
| this.scrollDirection = Axis.horizontal, | |
| this.reverse = false, | |
| this.physics, | |
| this.pageSnapping = true, | |
| this.dragStartBehavior = DragStartBehavior.start, | |
| this.allowImplicitScrolling = false, | |
| this.restorationId, | |
| this.clipBehavior = Clip.hardEdge, | |
| this.hitTestBehavior = HitTestBehavior.opaque, | |
| this.scrollBehavior, | |
| this.padEnds = true, | |
| }); | |
| final SmartPageViewController controller; | |
| final int initialPage; | |
| final List<Widget> children; | |
| final Duration duration; | |
| final Curve curve; | |
| final ValueChanged<int>? onPageChanged; | |
| // page view | |
| final Axis scrollDirection; | |
| final bool reverse; | |
| final ScrollPhysics? physics; | |
| final bool pageSnapping; | |
| final bool padEnds; | |
| final ScrollBehavior? scrollBehavior; | |
| final Clip clipBehavior; | |
| final HitTestBehavior hitTestBehavior; | |
| final String? restorationId; | |
| final bool allowImplicitScrolling; | |
| final DragStartBehavior dragStartBehavior; | |
| @override | |
| Widget build(BuildContext context) => LayoutBuilder( | |
| builder: (context, constraints) => _SmartPageSwitcherImpl( | |
| controller: controller, | |
| initialPage: initialPage, | |
| constraints: constraints, | |
| duration: duration, | |
| curve: curve, | |
| onPageChanged: onPageChanged, | |
| // page view | |
| scrollDirection: scrollDirection, | |
| reverse: reverse, | |
| physics: physics, | |
| pageSnapping: pageSnapping, | |
| dragStartBehavior: dragStartBehavior, | |
| allowImplicitScrolling: allowImplicitScrolling, | |
| restorationId: restorationId, | |
| clipBehavior: clipBehavior, | |
| hitTestBehavior: hitTestBehavior, | |
| scrollBehavior: scrollBehavior, | |
| padEnds: padEnds, | |
| // page view end | |
| children: children, | |
| ), | |
| ); | |
| } | |
| class _SmartPageSwitcherImpl extends StatefulWidget { | |
| const _SmartPageSwitcherImpl({ | |
| super.key, | |
| required this.controller, | |
| required this.constraints, | |
| required this.duration, | |
| this.initialPage = 0, | |
| this.curve = smartPageSwitcherCurve, | |
| this.onPageChanged, | |
| required this.children, | |
| // page view | |
| this.scrollDirection = Axis.horizontal, | |
| this.reverse = false, | |
| this.physics, | |
| this.pageSnapping = true, | |
| this.dragStartBehavior = DragStartBehavior.start, | |
| this.allowImplicitScrolling = false, | |
| this.restorationId, | |
| this.clipBehavior = Clip.hardEdge, | |
| this.hitTestBehavior = HitTestBehavior.opaque, | |
| this.scrollBehavior, | |
| this.padEnds = true, | |
| }) : assert(children.length > 0); | |
| final SmartPageViewController controller; | |
| final int initialPage; | |
| final Curve curve; | |
| final Duration duration; | |
| final BoxConstraints constraints; | |
| final ValueChanged<int>? onPageChanged; | |
| final List<Widget> children; | |
| // page view | |
| final Axis scrollDirection; | |
| final bool reverse; | |
| final ScrollPhysics? physics; | |
| final bool pageSnapping; | |
| final bool padEnds; | |
| final ScrollBehavior? scrollBehavior; | |
| final Clip clipBehavior; | |
| final HitTestBehavior hitTestBehavior; | |
| final String? restorationId; | |
| final bool allowImplicitScrolling; | |
| final DragStartBehavior dragStartBehavior; | |
| @override | |
| State<_SmartPageSwitcherImpl> createState() => _SmartPageSwitcherImplState(); | |
| } | |
| class _SmartPageSwitcherImplState extends State<_SmartPageSwitcherImpl> { | |
| late final _controller = PageController(initialPage: widget.initialPage); | |
| UnmodifiableListView<int> get indexes => UnmodifiableListView(_indexes); | |
| final _indexes = <int>[]; | |
| set indexes(List<int> value) => setState( | |
| () => _indexes | |
| ..clear() | |
| ..addAll(value), | |
| ); | |
| bool get ignorePointer => _ignorePointer; | |
| @protected | |
| var _ignorePointer = false; | |
| set ignorePointer(bool value) { | |
| if (_ignorePointer != value) { | |
| setState(() => _ignorePointer = value); | |
| } | |
| } | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _indexes.addAll(List.generate(widget.children.length, (i) => i)); | |
| widget.controller._state = this; | |
| } | |
| @override | |
| void dispose() { | |
| widget.controller._state = null; | |
| super.dispose(); | |
| } | |
| @override | |
| void didUpdateWidget(covariant _SmartPageSwitcherImpl oldWidget) { | |
| super.didUpdateWidget(oldWidget); | |
| _resetIndexes(); | |
| } | |
| void _resetIndexes() { | |
| if (!_ignorePointer) { | |
| indexes = List.generate(widget.children.length, (i) => i); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) => AbsorbPointer( | |
| absorbing: _ignorePointer, | |
| child: PageView( | |
| scrollDirection: widget.scrollDirection, | |
| reverse: widget.reverse, | |
| controller: _controller, | |
| physics: widget.physics, | |
| pageSnapping: widget.pageSnapping, | |
| onPageChanged: (p) { | |
| if (!_ignorePointer) { | |
| widget.onPageChanged?.call(p); | |
| } | |
| }, | |
| dragStartBehavior: widget.dragStartBehavior, | |
| allowImplicitScrolling: widget.allowImplicitScrolling, | |
| restorationId: widget.restorationId, | |
| clipBehavior: widget.clipBehavior, | |
| hitTestBehavior: widget.hitTestBehavior, | |
| scrollBehavior: widget.scrollBehavior, | |
| padEnds: widget.padEnds, | |
| children: [for (final index in _indexes) widget.children[index]], | |
| ), | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment