Photo by Lorenzo Dominici on Unsplash

After exploring the widgets lifecycle, we launch a series of posts about Flutter animations. On this first article, we will see how to build a simple page carousel.

Intro

If you’ve ever needed a pager with custom animations when swiping between pages, then you’ve probably searched for it on pub.dev too… Although there are some very interesting packages in there, like flutter swiper, sometimes they do not cover all the functionalities you may require. This one, for instance, does not allow us to jump to another page when applying “hand-made” transitions . On the other hand, you may also wonder… “Do I really need a package for this? Being Flutter, it should not be that hard, right..?” Well… let’s find out!

The usual suspects: widgets and classes from the Flutter API

Which are the widgets required to build a bouncing page carousel…?

“PageView” widget

As you probably already know, the “PageView” (or pager) component displays scrollable pages that can be swiped either horizontally or vertically.

PageView. Source: https://androidmonks.com/

Among other properties, it allows us to set:

  • controller: used to keep track of the page displayed, select an initial page to display (not necessarily the 1st one) or navigate to another item inside the group
  • pageSnapping: property that defines if the swiping is done page by page (when true). When set to false, every swipe gesture is analyzed and depending on its length, velocity, etc. the amount of pages swiped will vary
  • onPageChanged: callback invoked every time a new page is loaded in the central position
  • physics: object used to simulate a spring behaviour, among other physical models. If we set a “BouncingScrollPhysics“, then we can also define the amount of resistance against the motion using its frictionFactor property

“PageController” class

The controller instance manages the jumping between pages and stores the current page displayed in the pager using a double value. So by using it, and combined with the pageSnapping property, we can track the “mid” pages when, for instance, a page is being swiped from.

“Transform” widget

The “Transform” element is used to apply modifications (in shape, size, position or point of view) to another widget, which is nested as its child. So it basically acts as a wrapper around the child component: instead of actually applying the transformations, the widget delegates this task to another object.

When wrapping a widget inside a “Transform” widget, we can:

  • modify the position of the inner widget, using translation
  • increase or decrease the size of the inner widget, using scale
  • make the inner widget spin around, using rotate

This class has several factory constructors, so the most common operations can be invoked directly:

Transform.scale(...);
Transform.rotate(...);

Nevertheless, the default constructor also comes in handy, because it allows us to chain several transformations:

Transform(
   transform: ...,
   child: ...
);

From the previous snippet, we can see the main properties for this widget:

  • transform: allows us to specify the transformation we want to apply on the child widget
  • child: target widget that will be transformed

Since the “Transform” widget is just a wrapper, then all the mathematical operations involved when translating, scaling, rotating must be performed by another component, right…?

“Matrix4” class

This class executes all the operations when it comes to translate, scale, rotate… It provides a simple and robust API that allows us to state what we need (“Hey, I want you to translate this widget…”) and forget about the implementation details.

Any algebraic operation is represented and performed using this abstraction. Matrixes are efficient and performant data structures, so they are frequently used in computer graphics, and Flutter is no exception.

When using this class to apply transformations, the “identity()” method is usually invoked at the beginning of any operation in order to reset any remaining state from previous transformations.

One more thing to take into account: when it comes down to rotations, angles and all this fancy stuff, the class uses radians instead of degrees.

Matrix underlying behaviour

A matrix is just a multidimensional array (so it has rows and columns):

Matrix with m rows and n columns. Source: wikipedia

When applying some operation in this class, let’s say a scale transformation, for instance:

Matrix4.identity()..scale(2.0);

…then the matrix is “filled” with the data for the current component being transformed. After that, the current operation is applied, in this case, this means multiplying each value on the matrix with the scalar 2.0. As a result, it could be something like:

Scalar multiplication. Source: wikipedia

Note: that IS NOT the exact behaviour of the class, but I think this approach gives us an idea of how is working under the hood. If you want further exploration, check the API and some articles like this one.

“PageView” carousel

The small pieces

So we basically have 3 problems to address (as usual, divide and conquer to the rescue!):

  • obtain a dynamic value that allow us to modify the page orientation on the fly, as the user swipes the pages. We could set up a gesture detector or a scroll notification for this, but it’s simpler if we apply some math on the page values instead, because we already have them at hand.
  • detect the direction we are going (either from first page to last page or the other way around). Again, we can play with the page numbers received by the “onPageChanged” callback to set this variable.
  • modify the position and the orientation of the pages inside the pager. As promised in the widget of the week video, “Transform” should do all the heavy-lifting in this part.

Putting it all together…

The carousel implementation uses a stateful widget, because we want to force the rebuild (and redraw) of the widget every time the page value changes as a result of swiping pages. We will use this value to animate the pages and create the illusion of motion.

So, first of all, we have to keep track of the swipe event between pages using a listener, and set a new state every time the page value stored in the controller (of double data type) changes:

PageController _cntrl;
double _partialPage = 0.0;

...
    
this._cntrl.addListener(() {

      //XXX: store the "partial" page and force a rebuild    
      this.setState(() {
        this._partialPage = this._cntrl.page;
      }
    );

We also have to mantain the page value (of int data type) provided by the pager widget:

int _page = 0;
bool _goingForward = true;    

...
    
Widget build(BuildContext cntxt) {
    return PageView.builder(
        onPageChanged: (int newPage) {
          this._storeCurrentPage(newPage);
        },
        ...
    );
  }

  void _storeCurrentPage(int newPage) {
    this.setState(() {
      this._goingForward = (this._page < newPage);

      this._page = newPage;
    });
  }

This way, every time we navigate to a new page, we are also storing the direction we are moving, by comparing the new page value with the previous value. This direction value can be used to apply more “tailored” animations when swiping pages.

On the other hand, using all the page data we have stored (both double and int values for page numbers), we can calculate an offset. For instance, let’s say our inner state at any given point is:

//XXX: sample data when moving from page 1 to page 2...
int _page = 1;
bool _partialPage = 1.66;    

final offset = (_partialPage - _page);//XXX: so approx a 2/3 offset

Once we have the offset, we can change its sign, only use its absolute value, increase or decrease it using some weight factor… Keep in mind that, since we are updating these values on every swipe event, the offset will be different from frame to frame, so we can use it as a dynamic provider for our animation. The process doing so will be similar to the one when using tween animations.

Finally, we also have to set the pages we are moving from (origin) and we are moving to (destination). This involves taking into account the direction we are moving and the current page, but most of the times is a simple operation like:

int fromIndex = 0;
int toIndex = 0;

if (this._goingForward) fromIndex = this._partialPage.floor().toInt();

//XXX: more comparisons here...

Any item remaining in our backlog…? Well, we’ve made it so far, so the only missing tasks are:

  • build any widget we want to use as single pages inside the “PageView”
  • wrap every page with a “Transform” widget and set rotation transformations using the offset value we calculated before
return Transform(
      transform: Matrix4.identity()
        ..rotateY(angle)
        ..rotateZ(angle),
      child: CarouselPage(
        color: color,
        text: text,
        onMove: onMove,
      ),
    );

The “CarouselPage” from the snippet is built only for testing purposes, hence its structure using fake colums (so we have some visual dividers on every page just in case we want to check the log output) and the colors (in order to see when a page goes from “being swiped from” to “offscreen page”, for instance):

return Container(
        height: MediaQuery.of(cntxt).size.height,
        color: color,
        child: Row(
            children: [
              Flexible(
                flex: 1,
                child: Container(
                  decoration: BoxDecoration(border: Border.all()),
                ),
              ),
              //XXX: more cols here...
              ...
            ]
         )
      );    

And that would be all! Let’s check the final result:

Our bouncing carousel!

Wrapping up

So at the end, it was not all that hard! By wrapping the “PageView” pages inside a “Transform” widget, we can modify its appearance. When using them inside a stateful widget that rebuilds every time a swipe event is fired, we can get a “stream” of data used to animate our carousel.

Write you next time! As usual, check this repo for the full source code:

https://github.com/begomez/Flutter-Carousel-Widget

Join the Conversation

1 Comment

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: