Ahsin Irshad
Senior Flutter Developer | Ex. Native Android Developer
Build a Custom Bottom Navigation Bar in Flutter with Animated Icons from Rive | Flutter Community | Jan, 2024
Today I will show you how to build this custom bottom navigation bar in Flutter with animated icons from Rive.
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.
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,
),
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.
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,
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.
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.
#Build #Custom #Bottom #Navigation #Bar #Flutter #Animated #Icons #Rive #Flutter #Flutter #Community #Jan #Ahsin #Irshad