Reputation: 485
I have a PageView, how can I disable the left or right scrolling. I know by using NeverScrollableScrollPhysics we can disable the scrolling but how to disable the scrolling in one direction.
Upvotes: 11
Views: 14791
Reputation: 408
I tried the solution of diegoveloper and I found a more clean way to do it. This one locks the inverted scroll, it depends on the orientation, that's the reason why i havent used right or left, cause if you are using a listview up and down it will block the scroll up
class DirectionalScrollPhysics extends ScrollPhysics {
DirectionalScrollPhysics({
this.inverted = false,
ScrollPhysics? parent,
}) : super(parent: parent);
final bool inverted;
@override
DirectionalScrollPhysics applyTo(ScrollPhysics? ancestor) {
return DirectionalScrollPhysics(
inverted: inverted, parent: buildParent(ancestor));
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
final bool condition = this.inverted ? value < 0 : value > 0;
if (condition) return value;
return 0.0;
}
}
Then on your widget
ListView(
scrollDirection: Axis.horizontal,
physics: DirectionalScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
children: [],
),
Upvotes: 0
Reputation: 1470
Use ScrollController
and NotificationListener
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(backgroundColor: Colors.black, body: MyWidget()));
}
}
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
//Add this ScrollController
final ScrollController scrollController = ScrollController();
bool stop = false;
@override
Widget build(BuildContext context) {
return Stack(
children: [
//Add this NotificationListener
NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
print('${notification.scrollDelta}');
if (notification.scrollDelta! < 0) {
setState(() {
stop = true;
});
//Add this jumpTo
scrollController.jumpTo(
notification.metrics.pixels - notification.scrollDelta!);
} else if (notification.scrollDelta! > 3) {
setState(() {
stop = false;
});
}
return true;
},
child: ListView.builder(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) {
return Container(
width: 100,
color: Colors.primaries[i % Colors.primaries.length]);
}),
),
if (stop)
Center(
child: Text(
'STOP',
style: Theme.of(context).textTheme.headline1,
))
],
);
}
}
Upvotes: 0
Reputation: 177
if you decide to use @Fabricio N. de Godoi's answer like me, there is a bug. For example after you blocked scroll right, scroll right will not work as expected. But just after that if you scroll the opposide direction page will scroll right.
And this is the solition; Find this code
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
}
And replace with
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
} else {
_lock = true;
}
Upvotes: 0
Reputation: 51
Here is another version of the @diegoveloper and @wdavies973 ideas. I got the necessity of being able to only scroll with some programming command, ignoring the user gesture. Instead of some complex workaround in the main Widget, I was able to lock the screen to scroll using the ScrollPhysics behavior. When is user interaction, the applyPhysicsToUserOffset
is called before applyBoundaryConditions
. With this in mind, here is my sample, hope it help someone.
Note: Once locked, it is possible to navigate the pages using the controller.animateTo(pageIndex)
/// Custom page scroll physics
// ignore: must_be_immutable
class CustomLockScrollPhysics extends ScrollPhysics {
/// Lock swipe on drag-drop gesture
/// If it is a user gesture, [applyPhysicsToUserOffset] is called before [applyBoundaryConditions];
/// If it is a programming gesture eg. `controller.animateTo(index)`, [applyPhysicsToUserOffset] is not called.
bool _lock = false;
/// Lock scroll to the left
final bool lockLeft;
/// Lock scroll to the right
final bool lockRight;
/// Creates physics for a [PageView].
/// [lockLeft] Lock scroll to the left
/// [lockRight] Lock scroll to the right
CustomLockScrollPhysics({ScrollPhysics parent, this.lockLeft = false, this.lockRight = false})
: super(parent: parent);
@override
CustomLockScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomLockScrollPhysics(parent: buildParent(ancestor), lockLeft: lockLeft, lockRight: lockRight);
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if ((lockRight && offset < 0) || (lockLeft && offset > 0)) {
_lock = true;
return 0.0;
}
return offset;
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
/*
* Handle the hard boundaries (min and max extents)
* (identical to ClampingScrollPhysics)
*/
// under-scroll
if (value < position.pixels && position.pixels <= position.minScrollExtent) {
return value - position.pixels;
}
// over-scroll
else if (position.maxScrollExtent <= position.pixels && position.pixels < value) {
return value - position.pixels;
}
// hit top edge
else if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) {
return value - position.pixels;
}
// hit bottom edge
else if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) {
return value - position.pixels;
}
var isGoingLeft = value <= position.pixels;
var isGoingRight = value >= position.pixels;
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
}
return 0.0;
}
}
Usage example:
@override
void initState() {
super.initState();
controller.tabController = TabController(initialIndex: 0, length: 3, vsync: this);
}
void nextTab() => controller.tabController.animateTo(controller.tabController.index + 1);
@override
void build(BuildContext context)
return TabBarView(
controller: controller.tabController,
physics: CustomLockScrollPhysics(lockLeft: true),
children: [ InkWell(onTap: nextTab), InkWell(onTap: nextTab), Container() ],
),
}
Final note: I tried everything to avoid using some variable in the class since ScrollPhysics is @immutable, but in this case, it was the only way to successfully know if the input was a user gesture.
Upvotes: 2
Reputation: 986
Came up with a slightly better approach for this.
This approach doesn't require instantiating new physics instances and can be updated dynamically.
Use the function parameter onAttemptDrag
to allow or deny swipe requests. Your code in this function should be efficient as it will get called many times per second (when scrolling). Additionally, you may want to add a flag in this function that allows requests of programmatic origin to go through. For example, in the case of a "next button", this physics implementation would also block the functions jumpTo(..)
and animateTo
from working, so you'd need to have a flag that will temporarily return a default true
for a specific page transition if the next button has been pressed. Let me know of any questions or ways to improve this.
class LockingPageScrollPhysics extends ScrollPhysics {
/// Requests whether a drag may occur from the page at index "from"
/// to the page at index "to". Return true to allow, false to deny.
final Function(int from, int to) onAttemptDrag;
/// Creates physics for a [PageView].
const LockingPageScrollPhysics(
{ScrollPhysics parent, @required this.onAttemptDrag})
: super(parent: parent);
@override
LockingPageScrollPhysics applyTo(ScrollPhysics ancestor) {
return LockingPageScrollPhysics(
parent: buildParent(ancestor), onAttemptDrag: onAttemptDrag);
}
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
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
/*
* Handle the hard boundaries (min and max extents)
* (identical to ClampingScrollPhysics)
*/
if (value < position.pixels && position.pixels <= position.minScrollExtent) // under-scroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // over-scroll
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
bool left = value < position.pixels;
int fromPage, toPage;
double overScroll = 0;
if (left) {
fromPage = position.pixels.ceil() ~/ position.viewportDimension;
toPage = value ~/ position.viewportDimension;
overScroll = value - fromPage * position.viewportDimension;
overScroll = overScroll.clamp(value - position.pixels, 0.0);
} else {
fromPage =
(position.pixels + position.viewportDimension).floor() ~/ position.viewportDimension;
toPage = (value + position.viewportDimension) ~/ position.viewportDimension;
overScroll = value - fromPage * position.viewportDimension;
overScroll = overScroll.clamp(0.0, value - position.pixels);
}
if (fromPage != toPage && !onAttemptDrag(fromPage, toPage)) {
return overScroll;
} else {
return super.applyBoundaryConditions(position, value);
}
}
Here's my implementation of PagePosition
: https://gist.github.com/wdavies973/e596b8bb6b7ef773522e169464b53779
Here's a library I've been working on that uses this modified controller: https://github.com/RobluScouting/FlutterBoardView.
Upvotes: 2
Reputation: 886
You can also use the horizontal_blocked_scroll_physics library which I recently wrote that will allow you to block the left and right movements.
Upvotes: 6
Reputation: 103551
You can create your own ScrollPhysics
to allow only go to the right:
class CustomScrollPhysics extends ScrollPhysics {
CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
bool isGoingLeft = false;
@override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
isGoingLeft = offset.sign < 0;
return offset;
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
//print("applyBoundaryConditions");
assert(() {
if (value == position.pixels) {
throw FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent)
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value)
// overscroll
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
if (!isGoingLeft) {
return value - position.pixels;
}
return 0.0;
}
}
Usage:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PageView.builder(
itemCount: 4,
physics: CustomScrollPhysics(),
itemBuilder: (context, index) => Center(
child: Text("Item $index"),
),
));
}
Upvotes: 8