Native scroll experience on Flutter
Implementing fling to your CustomPaint widget using Physics based animation
Flutter ListView widget is feature rich and performant for short dataset. However on displaying large dataset we can see visible janks and spike in memory consumption.
An performant solution to displaying large dataset is to paint them on a Canvas. Flutter has a exhaustive Canvas apis supporting most use cases required for 2D drawing (Drawing shapes, shadows, Text and even fragment shaders) with various blend modes. It is not as straight forward as using a ListView is and require figuring out a little to get started on Canvas drawing. Here is an useful article to get you started.
In this Article, we are going to look at supporting scroll and swipe gestures on canvas. Flutter provides a PanGestureRecognizer to capture the touch coordinates on both axis, which stops callback as soon as the finger is lift up.
As soon as user completes the swipe, we are given DragEndDetails on PanGestureRecognizer.onEnd. Let’s see how we can implement a fling gesture with this.
First we need to determine if the swipe was fast enough to be considered as fling.
RawGestureDetector( gestures: <Type, GestureRecognizerFactory>{ PanGestureRecognizer: GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>( () => PanGestureRecognizer(), (PanGestureRecognizer instance) { instance.onStart = (DragStartDetails d) { distanceX = distanceY = 0; onDragStartMillis = DateTime.now().millisecondsSinceEpoch; }; instance.onUpdate = (DragUpdateDetails _scrollDetails) { distanceX += _scrollDetails.delta.dx; distanceY += _scrollDetails.delta.dy; }; instance.onEnd = (DragEndDetails _dragEndDetails) { var dragEndTime = DateTime.now().millisecondsSinceEpoch; bool isfling = instance.isFlingGesture( VelocityEstimate( pixelsPerSecond: _dragEndDetails.velocity.pixelsPerSecond, confidence: 1, duration: Duration( milliseconds: dragEndTime - onDragStartMillis), offset: Offset(distanceX, distanceY)), PointerDeviceKind.touch); }; }) }, child: CustomPaint( painter: MyCustomPainter(), )
DragEndDetails provides an isFlingGesture method which returns true if the velocity of the swipe was enough to be considered as fling.
In case it’s a fling, we’ll start a simulation that will help us to animate the scroll. For scrollable widgets like ListView, ScrollView, Flutter uses ClampingScrollSimulation (Android) and BouncingScrollSimulation (iOS) as default scroll animations.
void startFling(DragEndDetails end) { var _flingController = AnimationController.unbounded(vsync: this); var sim = ClampingScrollSimulation( position: <currScrollPos>, velocity: end.velocity.pixelsPerSecond.dy.abs()); _flingController.addListener((){ var newScrollVal = _flingController.value; //repaint your CustomPainter with the new scroll value }); _flingController.animateWith(sim); }
Make sure you use AnimationController.unbounded() to create the controller instance, otherwise your animation values will be clamped to controller’s default lowerBound and upperBound values. Like other physics based animations you need not provide duration as its applied to controller by the Clamping simulation.
Now, what if user interrupts the animation and swipes with greater velocity? We’ll see about carried momentum in the next story.
I’m one of the engineers behind Zoho Tables, A No Code Work Management tool for organizing and tracking data with real time collaboration. It’s showcases cool Flutter use cases. Check it out!