Synchronising widget animations with the scroll of a PageView in Flutter
Hi Flutter lovers!
Today we’re going to see how we can hook an animation with the scroll progress of a PageView
in Flutter.
Have you ever needed to animate a widget according to the scroll of a PageView
? Well I did, and came up with a way of doing that, which is what I’m going to cover with this article.
What are we talking about?
A PageView
in Flutter is a widget which takes care of displaying a list of widgets like pages of a carousel. It takes care of snapping to the next page if you “scroll past the other half” and other really cool stuff.
You can learn more about these little guys in Deven Joshi’s article.
Lazy people can get the idea with this 1-minute-ish video from Flutter:
PageView
provides, like most of the widgets of the scrolling family, a way to listen to its status changes. Our goal here is to listen to these changes and transform some other widget(s) accordingly.
We are going to need to know what a ValueNotifier
is, too.
Basically, a ValueNotifier
is a class that holds a value and notifies who’s listening to it when this value changes. Check here for further documentation.
Roadmap
We are going to:
- Create a
NotifyingPageView
widget, which wraps a PageView and notifies the changes to the providedValueNotifier
. - Create a screen with a
ValueNotifier<double>
to pass to theNotifyingPageView
in order to get updates about the scroll status. - Complete the rest of the screen with the
NotifyingPageView
and our widget to transform. - Launch the app and see our work in action! 🚀
Creating the NotifyingPageView
Nothing fancy in the build
function here.
In the initState
method we instantiate the PageController
, which allows us to control and get information about the PageView
it’s attached to.
The interesting stuff happens in the _onScroll
method, let’s focus on that.
_pageController.page
is a double
which goes from 0.0
to the number of the pages minus one (counting is from zero right?), covering a continuous interval according to the scroll position.
That is, if we have 3 pages, the range would be between 0.0
and 2.0
, and for example, if we are at page 0
and dragging forward to page 1
, halfway we’re going to have _pageController.page == 0.5
and so on until we hit 1
.
We are sure we’ve reached a “complete” page when the two numbers are equal, that meaning, for example, 1.0 == 1
.
We achieve that with the clause in the if
statement. If the condition is satisfied, we keep track of the page we’re in.
We then update our ValueNotifier
with the difference between the actual page and the previous.
Depending on the direction of the scroll, values can be either positive or negative. When scrolling to the next page we’re going to have values ranging from 0
to 1
, while the range is going to be from 0
to -1
for backward scrolling.
NOTE: Using this logic the difference will snap to 0.0
the moment we hit the new page, take that in account when you’re using this value for your animations.
Before going to the next part, some of you might point out that we could update the _previousPage
using the onPageChange
event offered by PageView
, but here’s why I choose not to.
This is what the docs say about onPageChange
is
Called whenever the page in the center of the viewport changes.
This means that we don’t get all the values between a page and the other, and we would update the _previousPage
before we need (the centre changes way before the whole page comes in).
Creating the screen and the ValueNotifier
A simple StatefulWidget
, we create the ValueNotifier
and override the dispose
method in order to dispose it when the time of our screen will inesorably come.
Let’s complete the work.
Building the page with NotifyingPageView and the transforming widget
We create a Column
which two children:
- The
NotifyingPageView
, which is supplied with the notifier. - An
AnimatedBuilder
tuned to the _notifier, which builds its child by rotating it by2* pi * _notifier.value
.
Transform.rotate
allows to easily rotate a widget around its center by the supplied angle
in radians (hence the use the dear pi
from dart:math
).
Each time the notifier changes, the AnimatedBuilder
reacts accordingly.
The fact that the value snaps to 0.0
when the new page kicks in does not bother us, because the rotation is not affected by this kind of discontinuity.
A transition would suffer from the snap, though I suggest to use a different logic for updating the notifier.
See everything in action
Considerations
We’ve seen a simple way to update the ValueNotifier
and use it with an AnimatedBuilder.
You can easily choose different mathematical functions to determine the new value (to avoid the snap to 0
, for example).
You can also swap the ValueNotifier
with an AnimationController
to orchestrate more complex animations driven by a Tween
bound to the AnimationController
.
Like ValueNotifier
, you can set the value of an AnimationController
by assigning it to the its value
property. Just be sure to have a value between 0
and 1
in this case, since AnimationController
values need to be in that range.
You could also listen directly to theValueNotifier
and do everything you like with the value that it yields, which gives you unlimited possibilities!
Conclusions
That’s all for now! We’ve seen how to easily leverage Listenable
values and AnimatedBuilder
to transform our widgets accordingly to the scroll of a PageView
(or everything else that works in a similar manner).
I hope you’ve enjoyed the article! You can find the full code (without the sake of brevity) here on GitHub.
Until the next article, happy coding!