How to scroll multiple pages with page snapping with Flutter PageView? - flutter-animation

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.

Related

ScrollView inside GestureDetector: Dispatch Touch Events

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()
]);

Flutter: How to keep gesture events state after opening a simple dialog

I have a grid of Images and I want to open a simple dialog when I long press an Image and to be closed automatically when my finger no longer contacts with the screen (like Instagram quick image preview).
I attached LongPress event to all the images and it works fine so a dialog opens up when I long press an image however when I put my finger up nothing happens even though I attached events like onTapUp, onLongPressEnd, onPointerUp Because of the new opened dialog, All of those events are lost and no longer fires up.
I tried to add the pointer up events to the opened dialog instead but there is a catch, I must tap and release again in order to make it work because Flutter unable to recognize that my finger is already in contact with screen and the opened dialog caused flutter to forget about this fact.
You can insert an OverlayEntry into the Overlay stack by using Overlay.of(context).insert(overlayEntry).
In this overlay, you can catch gestures when required and take actions accordingly. As overlays always sit on top of anything else, the dialog will not cancel your long press gesture and you will be able to respond to longPressEnd.
You will only need to calculate which image has been pressed or use the Offset's provided by onTapDown and the position of the images.
To get the global position of your images, you can assign GlobalKey's to your images and get their global positions in the following way:
final RenderBox renderBox = globalKey.currentContext.findRenderObject() as RenderBox;
final Offset position = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
To get the position of your long press, you will need to store the position of onTapDown:
onTapDown: (details) => position = details.globalPosition
Now you have everything you need to figure out which bounds the long press happened in.
I found a way to make it work. It can be done with Overlay Widget.
In the widget with GestureDetector, when onLongPress is called, create an OverlayEntry object with your dialog, and insert it into Overlay.
When onLongPressEnd is called, call the remove function of OverlayEntry object.
// Implement a function to create OverlayEntry
OverlayEntry getMyOverlayEntry({
#required BuildContext context,
SomeData someData,
}) {
return OverlayEntry(
builder: (context) {
return AlertDialog(child: SomeWidgetAgain());
}
);
}
// In the widget where you want to support long press feature
OverlayEntry myOverayEntry;
GestureDetector(
onLongPress: () {
myOverayEntry = getMyOverlayEntry(context: context, someData: someData);
Overlay.of(context).insert(myOverayEntry);
},
onLongPressEnd: (details) => myOverayEntry?.remove(),
child: SomeWidgerHere(),
)
Here's the gist on Github:
https://gist.github.com/plateaukao/79aa39854dc4eabf1220bdfa9a0334b6
You can use AnimatedContainer and put a GestureDetector inside.
change width and height using setState and it's done.
Center(
child: AnimatedContainer(
width: containerWidth,
height: containerHeight,
color: Colors.red,
duration: Duration(seconds: 1),
child: GestureDetector(
onLongPress: (){
print("Long Press");
setState(() {
containerWidth = 200;
containerHeight = 200;
});
},
onLongPressUp: (){
print("On Long Press UP");
setState(() {
containerWidth = 100;
containerHeight = 100;
});
},
),
),
)

Partially viewable bottom sheet - flutter

Is it possible in flutter to have the bottom sheet partially viewable at an initial state and then be able to either expand/dismiss?
I've included a screenshot of an example that Google Maps implements.
Use the DraggableScrollableSheet widget with the Stack widget:
Here's the gist for the entire page in this^ GIF, or try the Codepen.
Here's the structure of the entire page:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
CustomGoogleMap(),
CustomHeader(),
DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
builder: (BuildContext context, ScrollController scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: CustomScrollViewContent(),
);
},
),
],
),
);
}
In the Stack:
- The Google map is the lower most layer.
- The Header (search field + horizontally scrolling chips) is above the map.
- The DraggableBottomSheet is above the Header.
Some useful parameters as defined in draggable_scrollable_sheet.dart:
/// The initial fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.5`.
final double initialChildSize;
/// The minimum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.25`.
final double minChildSize;
/// The maximum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `1.0`.
final double maxChildSize;
Edit: Thank you #Alejandro for pointing out the typo in the widget name :)
I will be implementing the same behaviour in the next few weeks and I will be referring to the backdrop implementation in Flutter Gallery, I was able to modify it previously to swipe to display and hide (with a peek area).
To be precise you can replicate the desired effect by changing this line of code in backdrop_demo.dart from Flutter Gallery :
void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating)// || _controller.status == AnimationStatus.completed)
return;
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
}
I have just commented the controller status check to allow the panel to be swipe-able.
I know this isn't the complete implementation you are looking for, but I hope this helps you in any way.

Flutter - How to controll the velocity of a ListView builder?

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) {
...
})

How to extend PageView to both sides with builder?

Using PageView.builder I can get an infinite list of pages, but only in one direction, i.e. it is finite in the other direction!
The default scrollDirection of a PageView is Axis.horizontal. So what I mean is that in the regular case I can only scroll infinitely to the right, but not to the left.
I want to be able to scroll infinitely in both directions. I have not found a way to do this, especially, because I would expect the itemBuilder to give out negative indices then, which I have never seen. That leads me to wondering whether this is implemented at all, but I am open to custom solutions and will try to come up with something aswell.
There's no official way of having an infinite scroll in both directions.
But you can instead use PageController's initialPage property. Setting it to an absurdly big value. And then use this value as your "index 0".
class MyHomePage extends StatelessWidget {
final PageController pageController = new PageController(initialPage: 4242);
#override
Widget build(BuildContext context) {
return new Scaffold(body: new PageView.builder(
controller: pageController,
itemBuilder: (context, _index) {
final index = _index - 4242;
return new Container(
margin: const EdgeInsets.all(9.0),
color: Colors.red,
child: new Center(
child: new Text(index.toString()),
),
);
},
));
}
}
I solved it pretty straight forward. Honestly, I must have been out of my mind writing the question and issueing the bounty.
// number is irrelevant
final initialPage = (
.161251195141521521142025 // :)
* 1e6,).round();
final itemCount = getSomeItemCount();
PageView.builder(
pageController: PageController(
initialPage: initialPage,
),
itemBuilder: (context, page) {
final index = itemCount - (initialPage - page - 1) % itemCount - 1;
return getPageContent(index);
},
);
I am not sure if I should give credit to Rémi Rousselet because I was using this method before he proposed his answer. I just wanted to mention him because this question is getting undeserved traffic and he helped me to solve my problem :)

Resources