Flutter: Adding A Progress Bar Animation In Page View

In this post we will build a page view widget that has a animated progress bar. When user moves from one page to another, the progress bar gets updated with animation.

Introduction

A PageView is a useful widget which can be used to create a scrollable list of pages. We can build the widgets in a book-like manner with the help of a page view.

Basic Setup Of PageView Widget

A simple implementation of PageView can look like this:

DecisionMentor app
import 'package:flutter/material.dart';

class MyPages extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        children: <Widget>[
          Container(
            color: Colors.greenAccent,
            child: Center(child: Text("Page 1"),),
          ),
          Container(
            color: Colors.blueAccent,
            child: Center(child: Text("Page 2"),),
          ),
          Container(
            color: Colors.amberAccent,
            child: Center(child: Text("Page 3"),),
          ),
          Container(
            color: Colors.purpleAccent,
            child: Center(child: Text("Page 4"),),
          ),
        ],
      ),
    );
  }
}

Our app will look as below:

Page View Setup
Page View Setup

Cool. Now that we have a working PageView, we want to add an animated progress bar here.

Adding A Progress Bar

We will show the progress bar in the top most section of our app. Let’s add an application bar to the Scaffold widget for this purpose.

...
Scaffold(
      appBar: AppBar(
        title: Container(
          color: Colors.transparent,
          child: Row(
            children: <Widget>[
              Container(
                  height: 6.0,
                  width: 20.0,
                  decoration: BoxDecoration(
                      color: Colors.white),
                ), 
              Expanded(
                child: Container(
                  height: 6.0,
                  width: double.infinity,
                  decoration: BoxDecoration(
                      color: Colors.cyanAccent),
                ),
              )
            ],
          ),
        ),
      ),
      body: PageView(
...

If you look at the children inside the Row widget, you can see that there are two items. The first one has a fixed width while the second one is wrapped in an Expanded widget with maximum possible width. The Expanded widget ensures to take the remaining space of the Row container.

Since these two children have the same height but different colors (white and cyanAccent), the first child will appear as a progress indicator.

Progress Bar Setup In Page View
Progress Bar Setup In Page View

All that we need to do now is update the value of the width for the first child with animation!

Setup For Animation

For animating the progress bar, we will make use of Animation, Animation Controller and Animated Widget. If you are new to the topic of animation in Flutter, you should first read our previous post.

It will be helpful to understand the core concepts of the animation framework.

Convert To StatefulWidget

To add the animation, let’s change our “MyPages” widget into a Stateful widget first.

...
class MyPages extends StatefulWidget {

  @override
  _MyPagesState createState() => _MyPagesState();
}

class _MyPagesState extends State<MyPages> with SingleTickerProviderStateMixin{
...

Add The Animation, Animation Controller And Animated Widget

...
class _MyPagesState extends State<MyPages> with SingleTickerProviderStateMixin{
  Animation<double> _progressAnimation;
  AnimationController _progressAnimcontroller;

  @override
  void initState() {
    super.initState();

    _progressAnimcontroller = AnimationController(
      duration: Duration(milliseconds: 200),
      vsync: this,
    );

    _progressAnimation = Tween<double>(begin: 0, end: 0)
    .animate(_progressAnimcontroller);
  }
...

Now that we have defined the animation and animation controller, it’s time to create the animated widget.

Create a class called “AnimatedProgressBar” that will listen to the progress animation.

import 'package:flutter/material.dart';

class AnimatedProgressBar extends AnimatedWidget {
  AnimatedProgressBar({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Container(      
      height: 6.0,
      width: animation.value,
      decoration: BoxDecoration(color: Colors.white),
    );
  }
}

The AnimatedProgressBar class looks pretty much same like the container that we built inside the Row earlier. Just that the widget’s width is now set to animation.value.

Next, we will replace the progress bar that we created previously with this AnimatedProgressBar.

...
Scaffold(
      appBar: AppBar(
        title: Container(
          color: Colors.transparent,
          child: Row(
            children: <Widget>[
              AnimatedProgressBar(
                animation: _progressAnimation,
              ),
              Expanded(
                child: Container(
                  height: 6.0,
                  width: double.infinity,
                  decoration: BoxDecoration(
                      color: Colors.cyanAccent),
                ),
              )
            ],
          ),
        ),
      ),
      body: 
...

Animating The Progress Bar

Now we are almost ready to animate our Progress Bar!

Next we will calculate the amount of progress that should be animated to on each page changed event. Based upon the calculation, we will be updating animation object and running the animation.

_setProgressAnim(double maxWidth, int curPageIndex) {
    setState(() {
      growStepWidth = maxWidth / totalPages;
      beginWidth = growStepWidth * (curPageIndex - 1);
      endWidth = growStepWidth * curPageIndex;

      _progressAnimation = Tween<double>(begin: beginWidth, end: endWidth)
          .animate(_progressAnimcontroller);
    });

    _progressAnimcontroller.forward();
  }

Call the _setProgressAnim function on the initState event and then again during the onPageChanged event of the PageView.

Our final code:

//MyPages widget

import 'package:flutter/material.dart';

class MyPages extends StatefulWidget {
  @override
  _MyPagesState createState() => _MyPagesState();
}

class _MyPagesState extends State<MyPages> with SingleTickerProviderStateMixin {
  Animation<double> _progressAnimation;
  AnimationController _progressAnimcontroller;

  @override
  void initState() {
    super.initState();

    _progressAnimcontroller = AnimationController(
      duration: Duration(milliseconds: 200),
      vsync: this,
    );

    _progressAnimation = Tween<double>(begin: beginWidth, end: endWidth)
        .animate(_progressAnimcontroller);

    _setProgressAnim(0, 1);
  }

  double growStepWidth, beginWidth, endWidth = 0.0;
  int totalPages = 4;

  _setProgressAnim(double maxWidth, int curPageIndex) {
    setState(() {
      growStepWidth = maxWidth / totalPages;
      beginWidth = growStepWidth * (curPageIndex - 1);
      endWidth = growStepWidth * curPageIndex;

      _progressAnimation = Tween<double>(begin: beginWidth, end: endWidth)
          .animate(_progressAnimcontroller);
    });

    _progressAnimcontroller.forward();
  }

  @override
  Widget build(BuildContext context) {
    var mediaQD = MediaQuery.of(context);
    var maxWidth = mediaQD.size.width;

    return Scaffold(
      appBar: AppBar(
        title: Container(
          color: Colors.transparent,
          child: Row(
            children: <Widget>[
              AnimatedProgressBar(
                animation: _progressAnimation,
              ),
              Expanded(
                child: Container(
                  height: 6.0,
                  width: double.infinity,
                  decoration: BoxDecoration(color: Colors.cyanAccent),
                ),
              )
            ],
          ),
        ),
      ),
      body: PageView(
        onPageChanged: (i) {
          //index i starts from 0!
          _progressAnimcontroller.reset(); //reset the animation first
          _setProgressAnim(maxWidth, i + 1);
        },
        children: <Widget>[
          Container(
            color: Colors.greenAccent,
            child: Center(
              child: Text("Page 1"),
            ),
          ),
          Container(
            color: Colors.blueAccent,
            child: Center(
              child: Text("Page 2"),
            ),
          ),
          Container(
            color: Colors.amberAccent,
            child: Center(
              child: Text("Page 3"),
            ),
          ),
          Container(
            color: Colors.purpleAccent,
            child: Center(
              child: Text("Page 4"),
            ),
          ),
        ],
      ),
    );
  }
}

//Animated Progress Bar
import 'package:flutter/material.dart';

class AnimatedProgressBar extends AnimatedWidget {
  AnimatedProgressBar({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Container(      
      height: 6.0,
      width: animation.value,
      decoration: BoxDecoration(color: Colors.white),
    );
  }
}

And our final output of progress animation in pageview application:

Final Page View With Progress Animation
Final Page View With Progress Animation