Build a Custom Bottom Navigation Bar in Flutter with Animated Icons from Rive

The Flutter Way
Flutter Community
Published in
6 min readJan 7, 2024

--

Today I will show you how to build this custom bottom navigation bar in Flutter with animated icons from Rive.

Preview of BottomNavigationBar with Animated Icons

Also created a video tutorial 🎬, Build Custom Bottom Nav in Flutter with Animated icons

Project Setup 🛠️

We begin our journey with an empty Flutter project, create an assets directory. The first step involves downloading animated icons from the Rive community. Then rename the file to animated-icons.riv and add it to the assets folder. It’s crucial to ensure that this file is correctly referenced under assets in the pubspec.yaml. The last step is add the Rive package in your project.

RiveModel

Let’s create a model called RiveModel with: src, artboard, and stateMachineName.

class RiveModel {
final String src, artboard, stateMachineName;

RiveModel({
required this.src,
required this.artboard,
required this.stateMachineName,
});
}

Now you may think, what exactly are artboard and stateMachineName? Let’s back to the animated icons. When you click the remix button, it’s like seeing the blueprint.

Rive Editor

You’ll notice that each icon is named, which is what we call the artboard Every artboard is linked to a state machine. Here, the name is TIMER_Interactivity. Also the state machine has two states: idle and active. These states are key because they let us control the animation of the icon.

Let’s create a variable named bottomNavItems to store all the items for our bottom navigation.

List<RiveModel> bottomNavItems = [
RiveModel(
src: "assets/animated-icons.riv",
artboard: "CHAT",
stateMachineName: "CHAT_Interactivity"),
RiveModel(
src: "assets/animated-icons.riv",
artboard: "SEARCH",
stateMachineName: "SEARCH_Interactivity"),
RiveModel(
src: "assets/animated-icons.riv",
artboard: "TIMER",
stateMachineName: "TIMER_Interactivity"),
RiveModel(
src: "assets/animated-icons.riv",
artboard: "BELL",
stateMachineName: "BELL_Interactivity"),
RiveModel(
src: "assets/animated-icons.riv",
artboard: "USER",
stateMachineName: "USER_Interactivity"),
];

Now, let’s get back to our main code and start the most fun part.

The Bottom Navigation Bar

Begin by creating a new StatefulWidget called BottomNavWithAnimatedIcons. Then, in the main.dart, set this it as the home of your application.

import 'package:flutter/material.dart';

const Color bottonNavBgColor = Color(0xFF17203A);

class BottonNavWithAnimatedIcons extends StatefulWidget {
const BottonNavWithAnimatedIcons({super.key});

@override
State<BottonNavWithAnimatedIcons> createState() =>
_BottonNavWithAnimatedIconsState();
}

class _BottonNavWithAnimatedIconsState
extends State<BottonNavWithAnimatedIcons> {
@override
Widget build(BuildContext context) {
return Scaffold(
// TODO: Botton Nva Bar
);
}
}

For that we are not gonna use the traditional BottomNavigationBar widget, instead use Container. Replace ToDo: Bottom Nav Bar with below code

bottomNavigationBar: SafeArea(
child: Container(
height: 56, //TODO: In Future remove the height
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: bottonNavBgColor.withOpacity(0.8),
borderRadius: const BorderRadius.all(Radius.circular(24)),
boxShadow: [
BoxShadow(
color: bottonNavBgColor.withOpacity(0.3),
offset: const Offset(0, 20),
blurRadius: 20,
),
],
),
// TODO: Animated Icons,
),
Preview of bottomNavigationBar

Now, let’s display the icons. We do this by using a Row whose children are generated through List.generate. Each icon having a height and width of 36. RiveAnimation.asset to define the source. Replace TODO: Animated Icons to below code

child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
bottomNavItems.length,
(index) => SizedBox(
height: 36,
width: 36,
child: RiveAnimation.asset(
bottomNavItems[index].src,
// TODO: Mention Artboard
),
),
),
),

Now it displays only one icon for all.

Initial Preview of Rive Animated Icons

This happens because our file contains all the icons, and we need to specify which one we want, using the artboard. Simply replace TODO: Mention Artboard with this

artboard: bottomNavItems[index].artboard,
Rive Icons auto animating

Control the Animation

You’ll notice that some icons are animating while others aren’t. To manage the animation, we need to set up a controller. For that create function called riveOnInIt, where wedefine the StateMachineController and then assign it using fromArtboard where we need to pass the artboard & stateMachineName. After that, our next step is to attach this controller to the artboard.

Once the controller set up, you might be wondering what exactly we’re going to control with it. To answer that, let’s head back to the Rive editor.

Animation Inputs

Under inputs, you’ll find an active checkbox. Setting this active to true triggers the animation. This is exactly what we’ll use to control our animation. In Rive, there are three types of inputs we can use: Numbers, Boolean, or Trigger.

To access that on code, we use the findInput, where we need to mention the name of the input. Here is our function

void riveOnInIt(Artboard artboard, {required String stateMachineName}) {
StateMachineController? controller =
StateMachineController.fromArtboard(artboard, stateMachineName);

artboard.addController(controller!);
controllers.add(controller);

riveIconInputs.add(controller.findInput<bool>('active') as SMIBool);
}

Now, you’ll notice some errors because we haven’t yet defined the controllers and riveIconInputs. Let’s do that

List<SMIBool> riveIconInputs = [];
List<StateMachineController?> controllers = [];
int selctedNavIndex = 0;

We store the controller to ensure they can be disposed of when no longer needed. The selectedNavIndex will be used later to navigate between different pages and for other purposes.

Returning to RiveAnimation, in the ‘onInit’ , we need to reference our riveOnInit’

onInit: (artboard) {
riveOnInIt(artboard, stateMachineName: riveIcon.stateMachineName);
},

Trigger the animation

The last thing we need to do is to trigger the animation when an icon is tapped. Let’s wrap our SizedBox with a GestureDetector. On tapping, this will set the input status to true. Each icon takes 1 second to complete it’s animation, then it repeats. So, we’ll stop the animation after one second, Using Future.delayed.

GestureDetector(
onTap: () {
riveIconInputs[index].change(true);
Future.delayed(
const Duration(seconds: 1),
() {
riveIconInputs[index].change(false);
},
);
setState(() {
selctedNavIndex = index;
});
},
child: SizedBox( .... ),
)

Animated Bar

You’ll notice that the selected icon have an animated bar at the top. Let’s go and create that.

class AnimatedBar extends StatelessWidget {
const AnimatedBar({
super.key,
required this.isActive,
});

final bool isActive;

@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 2),
height: 4,
width: isActive ? 20 : 0,
decoration: const BoxDecoration(
color: Color(0xFF81B4FF),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
);
}
}

The final step is to wrap the SizedBox with a Column and then position the AnimatedBar at the top.

Complete Code

Below is the complete code for your reference.

const Color bottonNavBgColor = Color(0xFF17203A);

class BottonNavWithAnimatedIcons extends StatefulWidget {
const BottonNavWithAnimatedIcons({super.key});

@override
State<BottonNavWithAnimatedIcons> createState() =>
_BottonNavWithAnimatedIconsState();
}

class _BottonNavWithAnimatedIconsState
extends State<BottonNavWithAnimatedIcons> {
List<SMIBool> riveIconInputs = [];
List<StateMachineController?> controllers = [];
int selctedNavIndex = 0;
List<String> pages = ["Chat", "Search", "History", "Notification", "Profile"];

void animateTheIcon(int index) {
riveIconInputs[index].change(true);
Future.delayed(
const Duration(seconds: 1),
() {
riveIconInputs[index].change(false);
},
);
}

void riveOnInIt(Artboard artboard, {required String stateMachineName}) {
StateMachineController? controller =
StateMachineController.fromArtboard(artboard, stateMachineName);

artboard.addController(controller!);
controllers.add(controller);

riveIconInputs.add(controller.findInput<bool>('active') as SMIBool);
}

@override
void dispose() {
for (var controller in controllers) {
controller?.dispose();
}
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(pages[selctedNavIndex])),
bottomNavigationBar: SafeArea(
child: Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: bottonNavBgColor.withOpacity(0.8),
borderRadius: const BorderRadius.all(Radius.circular(24)),
boxShadow: [
BoxShadow(
color: bottonNavBgColor.withOpacity(0.3),
offset: const Offset(0, 20),
blurRadius: 20,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
bottomNavItems.length,
(index) {
final riveIcon = bottomNavItems[index];
return GestureDetector(
onTap: () {
animateTheIcon(index);
setState(() {
selctedNavIndex = index;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBar(isActive: selctedNavIndex == index),
SizedBox(
height: 36,
width: 36,
child: Opacity(
opacity: selctedNavIndex == index ? 1 : 0.5,
child: RiveAnimation.asset(
riveIcon.src,
artboard: riveIcon.artboard,
onInit: (artboard) {
riveOnInIt(artboard,
stateMachineName: riveIcon.stateMachineName);
},
),
),
),
],
),
);
},
),
),
),
),
);
}
}

class AnimatedBar extends StatelessWidget {
const AnimatedBar({
super.key,
required this.isActive,
});

final bool isActive;

@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 2),
height: 4,
width: isActive ? 20 : 0,
decoration: const BoxDecoration(
color: Color(0xFF81B4FF),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
);
}
}

There’s more to explore!

This bottom navigation bar is just one component of the Animated Flutter App with Rive. For more advanced animations in Flutter, be sure to check out that one.

Thank you so much for reading. I hope you found this helpful. If you have any suggestions or feedback, please let me know. Your input is invaluable in helping me create better content for you all.

--

--

The Flutter Way
Flutter Community

Want to improve your flutter skill? Join our channel, learn how to become an expert flutter developer and land your next dream job!