Skip to content

Instantly share code, notes, and snippets.

@urusaich
Created November 25, 2025 11:43
Show Gist options
  • Select an option

  • Save urusaich/f7d1d194cce436aedaceaa7296d6c5d0 to your computer and use it in GitHub Desktop.

Select an option

Save urusaich/f7d1d194cce436aedaceaa7296d6c5d0 to your computer and use it in GitHub Desktop.
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