I was given a design to prototype in Android at work. Circle images that snap to the centre, and zoom into place.
In Android a ViewPager with a whole host of libraries, or a RecyclerView will do the trick, but I was curious. How can Flutter handle it?
As you can see from the GIF above, it handles it just fine. But I did have to play for quite a while to get it there.
Lets start with the circle cards:
Card(
elevation: 4,
clipBehavior: Clip.antiAlias,
shape: CircleBorder(side: BorderSide(color: Colors.grey.shade200, width: 5)),
child: Image.asset(
"assets/your_image.png",
fit: BoxFit.cover,
),
)
It’s really easy. Add a card, set its shape to a circle and add a border color / width. Inside the card add the image.
Next the pager:
Initially I just wanted to get the circles snapping, and showing half when next on screen. Simply adding a PageView
will not achieve this. We also need to provide a PageController
and set the viewportFraction
setting this to 0.5 achieved what I was looking for.
Initialise the PageController
and then add it to the PageView
:
PageView.build(
controller: pageController
itemBuilder: (context, index){
...
}
)
We’re getting close, but what about the zoom effect?
For this, first I searched Google, found a library, didn’t work as I needed. Then I found a few blogs, they were doing more complex animations. But they gave me the starting point NotificationListener
Adding a NotificationListener<ScrollNotification>
with the PageView
as its child, will deliver fine detailed scrolling information.
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification){
debugPrint(scrollNotification.toString());
},
child: PageView.build(
controller: pageController
itemBuilder: (context, index){
...
}
)
)
You’ll get a stream of information like this:
ScrollUpdateNotification(depth: 0 (local), PageMetrics(615.7..[414.0]..212.3), scrollDelta: 0.8587489211474804)
After trying to get my head around it for a bit, I realised … I don’t need that information. I just need to know about the scroll event. Every time the scroll notification fired, the thing I needed was pageController.page
which returns a double 0.2 we’re near position 0, 0.9 we’re near position 1… etc.
So I created a variable called page
and used that in state.
onNotification: (ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
setState(() {
page = pageController.page;
});
}
},
Now we know where we are in the scroll, time to zoom:
I had created a method which would return the circle card, this method took the image assets string, and a scale double.
Widget circleOffer(String image, double scale) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(bottom: 10),
height: PAGER_HEIGHT * scale,
width: PAGER_HEIGHT * scale,
child: Card(
elevation: 4,
clipBehavior: Clip.antiAlias,
shape: CircleBorder(side: BorderSide(color: Colors.grey.shade200, width: 5)),
child: Image.asset(
image,
fit: BoxFit.cover,
),
),
),
);
}
It adjusts the height and width, based on scale.
To calculate the scale, I used this line. It works well enough for me, I’m sure a good mathematician could do much better:
final scale =
max(SCALE_FRACTION, (FULL_SCALE - (index - page).abs()) + viewPortFraction);
So now in the builder for our PageView
we call to get circleOffer
with the calculated scale, and the animation will work.
Here’s a gist of the full class: