Flutter: Animated Progress/Count Bar

main.dart

import 'package:flutter/material.dart';
import 'package:progress_bar/progress_bar.dart';
import 'package:provider/provider.dart';

import 'animated_bar.dart';
import 'count_provider.dart';

void main() => runApp(const ProgressBarApp());

class ProgressBarApp extends StatelessWidget {
const ProgressBarApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
/// in an actual app, you'll be using MultiProvider
return ChangeNotifierProvider<CountProvider>(
create: (_) => CountProvider(),
child: const MaterialApp(home: Home()),
);
}
}

class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
/// listen: false if you do not want this widget to rebuild when the provider notifies its listeners
/// since I'm only creating the instance to use its methods, do not need to listen
final CountProvider _countProvider = Provider.of(context, listen: false);
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Builder(
/// use Builder + select so that only this widget will rebuild when the selected value changes in a provider
builder: (BuildContext ctx) {
final int _count = ctx.select<CountProvider, int>((CountProvider p) => p.count1);
return ProgressBar(count: _count);
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: const Icon(Icons.remove),
onPressed: _countProvider.removeCount1,
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _countProvider.addCount1,
),
],
),
),
const AnimatedBar(),
],
),
),
);
}
}

ProgressBar

import 'package:flutter/material.dart';

class ProgressBar extends StatelessWidget {
const ProgressBar({Key? key, required this.count}) : super(key: key);
final num count;

@override
Widget build(BuildContext context) {
final double _barWidth = MediaQuery.of(context).size.width * 0.7;

return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Stack(
children: <Widget>[
Container(
margin: const EdgeInsets.only(right: 20.0),
height: 20.0,
width: _barWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
color: const Color.fromRGBO(234, 234, 234, 1.0)
),
),
Container(
height: 20.0,
width: _barWidth * count/7,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
gradient: const LinearGradient(
colors: <Color>[
Color.fromRGBO(185, 235, 255, 1.0),
Color.fromRGBO(145, 224, 255, 1.0),
Color.fromRGBO(85, 206, 255, 1.0),
Colors.lightBlueAccent,
],
stops: [0.25, 0.5, 0.75, 1.0],
)
),
),
],
),
RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black, fontSize: 15.0, fontWeight: FontWeight.w600),
children: <InlineSpan>[
TextSpan(
text: count.roundToDouble().toString().substring(0, 1),
style: const TextStyle(color: Colors.lightBlueAccent, fontSize: 24.0),
),
const TextSpan(text: " / 7")
],
),
),
],
);
}
}

Count Provider

import 'package:flutter/foundation.dart';

class CountProvider extends ChangeNotifier {
/// for static progress bar
int _count1 = 0;
int get count1 => this._count1;
set count1(int i) => throw "error";

/// for animated progress bar
double _count2 = 6.0;
double get count2 => this._count2;
set count2(double i) => throw "error";

int _couponCount = 2;
int get couponCount => this._couponCount;
set couponCount(int i) => throw "error";

bool _needNewCoupon = false;
bool get needNewCoupon => this._needNewCoupon;
set needNewCoupon(bool b) => throw "error";

void addCount1(){
if (this._count1 == 7) return;
this._count1++;
this.notifyListeners();
}

void removeCount1(){
if (this._count1 == 0) return;
this._count1--;
this.notifyListeners();
}

/// called when progress bar is animating & completed
void changeCount(double count){
if (count == 0 && this._needNewCoupon) {
this._couponCount ++;
this._needNewCoupon = false;
this.notifyListeners();
}
this._count2 = count;
this.notifyListeners();
}

void resetCount(){
this._needNewCoupon = true;
this.notifyListeners();
}
}

Animated Progress Bar — UI and Setting the Animation Up

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Row(
children: <Widget>[
Builder(
builder: (BuildContext ctx) {
final int _stampCount = ctx.select<CountProvider, int>((CountProvider p) => p.couponCount);
if (_stampCount == 0) return const SizedBox();
return Container(
margin: const EdgeInsets.only(left: 30.0),
child: Row(
children: List.generate(_stampCount, (int i) => SizedBox(
width: 30.0,
child: Icon(Icons.local_attraction, size: 26.0),),
),
),
);
},
),
Builder(
builder: (BuildContext ctx) {
final bool _needNewStamp = ctx.select<CountProvider, bool>((CountProvider p) => p.needNewCoupon);
if (!_needNewStamp) return const SizedBox();
return Container(
width: 30.0,
child: Icon(Icons.local_attraction, size: this._stampAnimation?.value ?? 26.0),
);
},
),
],
),
Builder(
builder: (BuildContext ctx) {
final double _count = ctx.select<CountProvider, double>((CountProvider p) => p.count2);
return ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: ProgressBar(count: _count),
);
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: const Icon(Icons.remove),
onPressed: () async {
final int _count = this._countAnimation!.value.toInt();
if (_count == 0) return;
this._countAnimation = Tween<double>(begin: _count.toDouble(), end: _count - 1).animate(this._countAnimationController!);
this._countAnimationController?.reset();
await this._countAnimationController!.forward();
},
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
final int _count = this._countAnimation!.value.toInt();
this._countAnimation = Tween<double>(begin: _count.toDouble(), end: _count + 1).animate(this._countAnimationController!);
this._countAnimationController?.reset();
await this._countAnimationController!.forward();
},
),
],
),
),
],
);
}
Animation<double>? _countAnimation;
AnimationController? _countAnimationController;

@override
void initState() {
super.initState();
this._countAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))
..addListener(() async {
...(bunch of code)...
});
final double _count = context.read<CountProvider>().count2;
this._countAnimation = Tween<double>(begin: _count, end: _count+1).animate(this._countAnimationController!);
}

/// + button onPressed
() async {
final int _count = this._countAnimation!.value.toInt();
this._countAnimation = Tween<double>(begin: _count.toDouble(), end: _count + 1).animate(this._countAnimationController!);
this._countAnimationController?.reset();
await this._countAnimationController!.forward();
},

Animated Progress Bar — the Animation Itself

@override
void initState() {
super.initState();
this._countAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))
..addListener(() async {

if (this._countAnimationController!.isAnimating) {
context.read<CountProvider>().changeCount(this._countAnimation!.value);
}

if (this._countAnimationController!.isCompleted) {
context.read<CountProvider>().changeCount(this._countAnimation!.value);
if (context.read<CountProvider>().count2 == 7) {
await showDialog(
context: context,
builder: (BuildContext ctx) {
return Dialog(
child: Container(
alignment: Alignment.center,
height: 150.0,
width: 100.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Congratulations!\nYou have received a coupon!", style: TextStyle(fontSize: 16.0),),
TextButton(onPressed: Navigator.of(ctx).pop, child: Text("Close", style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600),))
],
)
),
);
},
);

this._countAnimation = Tween<double>(begin: 7.0, end: 0.0).animate(this._countAnimationController!);
this._countAnimationController?.reset();
context.read<CountProvider>().resetCount();
this._countAnimationController!.forward();
this._couponAnimationController!.forward();
}
}
});

this._couponAnimationController = AnimationController(vsync: this, duration: Duration(milliseconds: 600))
..addListener(() {
if (this._couponAnimationController!.isAnimating) this.setState(() {});
});
final double _count = context.read<CountProvider>().count2;
this._couponAnimation = Tween<double>(begin: 15.0, end: 26.0).animate(this._couponAnimationController!);
this._countAnimation = Tween<double>(begin: _count, end: _count+1).animate(this._countAnimationController!);
}

--

--

Flutter & Node.js Full-Stack Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store