I have a ListView.builder and I would like to control the speed of the scroll in the ListView but I couldn't find a solution for this besides extending the Simulation where I then override the velocity and then extend the ScrollingPhysics class and provide the velocity from there. But I couldn't figure out how I should do it.
Do you have any other solutions or an example for how to do this?
If you need android-like scroll behavior take a look at ClampingScrollSimulation's constructor parameter friction. For ScrollPhysics, it is a coefficient of scrolling deceleration. The more friction is, the sooner scroll view stops scrolling.
You can control friction in custom scroll physics class:
class CustomScrollPhysics extends ScrollPhysics {
const CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
...
#override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
final tolerance = this.tolerance;
if ((velocity.abs() < tolerance.velocity) ||
(velocity > 0.0 && position.pixels >= position.maxScrollExtent) ||
(velocity < 0.0 && position.pixels <= position.minScrollExtent)) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
friction: 0.5, // <--- HERE
tolerance: tolerance,
);
}
}
And use it for ScrollView subclass widget:
ListView.builder(
physics: CustomScrollPhysics(),
itemBuilder: (context, index) {
...
})
Related
I am using PageView.builder to display around 50 images which cover the whole viewport (viewportFraction: 1.0). The user should be able to scroll the images slowly by swiping slowly and fast by swiping fast. When the desired image is seen, the user should be able to "hold" the image which should then snap to the viewport. I have seen this type of behaviour in other apps but cannot figure out how to do it with Flutter.
pageSnapping:true fixes the desired snapping, but then one cannot scroll past several images with one swipe.
child: PageView.builder(
pageSnapping: false,
onPageChanged: (index) {print(index);} ,
controller: pageController,
scrollDirection: Axis.vertical,
itemCount: dogList.length,
itemBuilder: (context, index) {
image = dogList[index];
return SizedBox.expand(child: Image.file(image, fit:BoxFit.cover)
}),
When the scrolling is stopped by holding down the finger on the desired image, it typically stays with parts of two adjacent images shown.
Update: I found a solution. If I wrap the PageView.builder with a NotificationListener, I can detect the ScrollEndNotification and get the location from pageController.page.toInt(), save it and do a setState. Then in the build I can do pageController.animateToPage to the saved location.
This works reasonably well, the user experience takes some getting used to.
This should be an option on PageController or PageView, not requiring any coding.
Usage of plugins
What you are looking for is is a carousel behavior, there are already a few plugins available such as carousel_slider or some native flutter documentation to a photo-filter-carousel which also has the same behavior.
Based on your approach (Best option)
You already had a great idea with changing pageSnapping to false to disable the internal physics of PageView. Now we can easily extend (and overwrite the snap physics and also configure velocityPerOverscroll (logical pixels per second) for our needs.
PageView(
pageSnapping: false,
physics: const PageOverscrollPhysics(velocityPerOverscroll: 1000),
Overwritten Snap-Phyics
class PageOverscrollPhysics extends ScrollPhysics {
///The logical pixels per second until a page is overscrolled.
///A satisfying value can be determined by experimentation.
///
///Example:
///If the user scroll velocity is 3500 pixel/second and [velocityPerOverscroll]=
///1000, then 3.5 pages will be overscrolled/skipped.
final double velocityPerOverscroll;
const PageOverscrollPhysics({
ScrollPhysics? parent,
this.velocityPerOverscroll = 1000,
}) : super(parent: parent);
#override
PageOverscrollPhysics applyTo(ScrollPhysics? ancestor) {
return PageOverscrollPhysics(
parent: buildParent(ancestor)!,
);
}
double _getTargetPixels(ScrollMetrics position, double velocity) {
double page = position.pixels / position.viewportDimension;
page += velocity / velocityPerOverscroll;
double pixels = page.roundToDouble() * position.viewportDimension;
return pixels;
}
#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 double target = _getTargetPixels(position, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
}
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Although PageOverscrollPhysics might look crazy complicated, it's essentially just an adjustment of the PageScrollPhysics()class.
I have a GestureDetector that´s responsible for dragging a container up and down, to change the height. The contents of the container may be too long, so the content must be scrolled.
I can´t figure out how to dispatch the touch event to the correct component, I tried it with a IgnorePointer and change the ignoring property.
class _SlideSheetState extends State<SlideSheet>
bool _ignoreScrolling = true;
GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
if(isDraggedUp) {
setState(() {
_ignoreScrolling = false
});
}
// update height of container, omitted for simplicity
},
child: NotificationListener(
onNotification: (ScrollNotification notification) {
if(notification is OverscrollNotification) {
if(notification.overscroll < 0) {
// the scrollview is scrolled to top
setState(() {
_ignoreScrolling = true;
});
}
}
},
child: IgnorePointer(
ignoring: _ignoreScrolling,
child: SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Container(
// ...
)
)
)
)
Does anybody know a good way to dispatch touch events up or down the Widget tree? Because in my solution, obviously, you always have to make one touch event just to change the "listener" from GestureDetector to SingleChildScrollView, which is annoying for the user, to say the least.
I was working on the same kind of widget today. You don't need the GestureDetector containing the NotificationListener. It's redundant and from my experience overrides the scrollListener within it or under it (depending on if you place it in a parent/child scenario or a stack scenario). Handle everything within the NotificationListener itself. Including updating your container's height. If you need the scrollable container to grow before you can scroll then I put mine in a stack with an "expanded" bool which then reactively built a gesture detector on top of the scroll container. Then when it was expanded I used the NotificationListener to handle it's drag displacement.
Stack(children:[
NotificationListener(/* scroll view stuff */),
expanded ? GestureDetector() : Container()
]);
I'm currently learning Flutter and I'm having some trouble showing a Snackbar after the interaction with the slider has ended (in other words, the final value was set when the user lifts their finger off the slider). I can't call my _showSnackBar() method in onChange because the snackbar is created and shown many times, one after the other.
Is there something I can do to call a method only after the interaction has finished? I was thinking of making a pull request and add something like onInteractionEnded callback property, but I would like to find out of there is another way first.
Here is my code for reference.
class _MySliderState extends State<MySlider> {
int _value = 2;
#override
Widget build(BuildContext context) {
return Slider(
min: 0.0,
max: 4.0,
divisions: 4,
value: (_value * 1.0),
onChanged: (double value) {
setState(() {
_value = value ~/ 1;
});
_showSnackBar();
},
);
}
void _showSnackBar() {
var snackbar = SnackBar(content: const Text('Slider value changed'));
Scaffold.of(context).showSnackBar(snackbar);
}
}
Thanks.
onChangeStart and onChangeEnd was added to Slider very recently
https://github.com/flutter/flutter/pull/17298
The change should available in master already.
I'm trying to display a widget once I have info about the max scroll extent. I can find that number if I assign an instance of ScrollController to the controller property of a scrollable widget.
My problem is that the ScrollController gets attached to the scrollable widget during the build, so I can not use the max scroll extent number before the first build. Thus what I was trying to do is display an empty Container in the first build and then switch that empty Container with the widget I actually want. Something like this:
_scrollController.positions.length == 0 ? new Container() : new Align(
alignment: Alignment.center,
child: new Container(
width: constraints.maxWidth,
height: 50.0,
color: Colors.black,
)
)
Now this does not work of course because _scrollController.positions.length will be 0 at the beginning and nowhere do I call setState when this value changes (when the controller gets attached).
So my question: Is there a place where I can get notified whenever the ScrollController gets attached to a scrollable widget? Or is there a better approach for this?
If the scrollable is widget.child.
#override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.child,
);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
widget.child.update(notification.metrics);
}
return false;
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {
applyInitialScrollPosition();
}
void applyInitialScrollPosition() {
ScrollController scrollControler = widget.child.controller;
ScrollPosition position = scrollControler.position;
ScrollNotification notification = ScrollUpdateNotification(
metrics: FixedScrollMetrics(
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
pixels: position.pixels,
viewportDimension: position.viewportDimension,
axisDirection: position.axisDirection),
context: null,
scrollDelta: 0.0);
_handleScrollNotification(notification);
}
The child must extends ChangeNotifier and has an update method:
void update(ScrollMetrics metrics) {
assert(metrics != null);
_lastMetrics = metrics; // Save the metrics.
notifyListeners();
}
All this only works if a scroll controller has explicitly been defined for the scrollable (widget.child).
I'm trying to implement double-tap-to-zoom in my zoomable_images plugin but the GestureTapCallback doesn't provide the tap location information.
Ideally the offset would be returned by the callback. Is there another API for this?
You can provide a GestureTapDownCallback callback as the onTapDown argument of the GestureDetector constructor. The GestureTapDownCallback takes a TapDownDetails argument that includes the global position of the tap. You can then convert it to relative coordinates using BuildContext.findRenderObject and RenderBox.globalToLocal:
Offset _tapPosition;
void _handleTapDown(TapDownDetails details) {
final RenderBox referenceBox = context.findRenderObject();
setState(() {
_tapPosition = referenceBox.globalToLocal(details.globalPosition);
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
/* ... */
onTapDown: _handleTapDown,
);
}
In your onDoubleTap handler, you can reference _tapPosition to find out where the most recent tap was located.
For an example of this in action, see InkWell.
as of [✓] Flutter (Channel stable, 2.5.3)
GestureDetector(
onTapDown: (details) {
var position = details.globalPosition;
// you can also check out details.localPosition;
if (position.dx < MediaQuery.of(context).size.width / 2){
// tap left side
} else {
// tap rigth size
}
},
child: SomeChildWidget(),
),
If you want to handle double taps, you'll need to store the tap position from the onDoubleTapDown and then work with onDoubleTap:
late Offset _doubleTapPosition;
...
onDoubleTap: () {
//do your stuff with _doubleTapPosition here
},
onDoubleTapDown: (details) {
final RenderBox box = context.findRenderObject() as RenderBox;
_doubleTapPosition = box.globalToLocal(details.globalPosition);
},
Original answer