Animated Icons: The Missing Piece to Your Bottom Nav in Flutter & Rive

The Flutter Way
Flutter Community
Published in
8 min readJan 31, 2023

--

Welcome to episode 3 of Rive and Flutter: A Match Made in Animation Heaven, where we will explore the process of creating a custom bottom navigation bar, adding animated icons and an animated indicator for the selected tab, which can greatly improve the overall design and functionality of the app. So, if you want to take your mobile app design to the next level, read on to learn how to create a visually appealing and functional bottom navigation bar using Flutter and Rive Animation.

Bottom Navigation Bar with Animated Icons

Here is the link to Episode 2 in case you missed it

🎬 Video tutorial

Bottom Navigation Bar

Begin by creating a new file named entry_point.dart within the lib directory. Then, create a stateful widget, named EntryPoint .

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

@override
State<EntryPoint> createState() => _EntryPointState();
}

class _EntryPointState extends State<EntryPoint> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
bottomNavigationBar: SafeArea(
child: Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: backgroundColor2.withOpacity(0.8),
borderRadius: const BorderRadius.all(Radius.circular(24)),
),
child: Row(
// TODO: Set mainAxisAlignment
children: [
// TODO: Bottom nav items
],
),
),
),
);
}
}

In this case, the Scaffold body is an empty container, while the bottomNavigationBar is a container with added margin and padding. Additionally, a background color and border radius have been applied to the BoxDecoration. The child of the container is a Row, but it currently has no children. We will add the bottom navigation items to this area later.

Preview

Animated Icons

Before incorporating any animated icons into the bottom navigation bar, let’s first create a model for those icons, which we will name RiveAsset.

import 'package:rive/rive.dart';

class RiveAsset {
final String artboard, stateMachineName, title, src;
late SMIBool? input;

RiveAsset(this.src,
{required this.artboard,
required this.stateMachineName,
required this.title,
this.input});

set setInput(SMIBool status) {
input = status;
}
}

In the src field, specify the asset name or URL. In this example, we are using animated icons created by the Rive community, Animated Icon Set — 1.

Animated Icon Set 1 — Preview on Rive

If you open it on Rive you will notice, each animation has its own artboard and state machine name. Additionally, each icon has an input that is used to identify whether the animation is currently playing or not.

Let’s create a variable called bottomNavs which is a List of RiveAsset. This list will contain all the icons that we will use in our bottom navigation bar.

List<RiveAsset> bottomNavs = [
RiveAsset("assets/RiveAssets/icons.riv",
artboard: "CHAT", stateMachineName: "CHAT_Interactivity", title: "Chat"),
RiveAsset("assets/RiveAssets/icons.riv",
artboard: "SEARCH",
stateMachineName: "SEARCH_Interactivity",
title: "Search"),
RiveAsset("assets/RiveAssets/icons.riv",
artboard: "TIMER",
stateMachineName: "TIMER_Interactivity",
title: "Chat"),
RiveAsset("assets/RiveAssets/icons.riv",
artboard: "BELL",
stateMachineName: "BELL_Interactivity",
title: "Notifications"),
RiveAsset("assets/RiveAssets/icons.riv",
artboard: "USER",
stateMachineName: "USER_Interactivity",
title: "Profile"),
];

The Animated Icon Set — 1 has already been added to the assets directory as a icons.riv file, and here we will utilize it. This one file contains all the animated icons. To specify which icon we want to use, we can define the artboard and stateMachineName.

Create a new variable called selectedBottomNav and set the first bottom navigation item as the default selection.

RiveAsset selectedBottomNav = bottomNavs.first;

Replace TODO: Bottom nav items with following code

...List.generate(
bottomNavs.length,
(index) => GestureDetector(
onTap: () {
// TODO: Play animation on tap
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// TODO: Animated Bar
SizedBox(
height: 36,
width: 36,
child: Opacity(
opacity: 1, // TODO: Chnage Opacity if not selected
child: RiveAnimation.asset(
bottomNavs[index].src,
artboard: bottomNavs[index].artboard,
onInit: (artboard) {
// TODO: Set the input value
},
),
),
),
],
),
),
)

Adding a Rive asset to our app is similar to adding an image, where we define the source. We will wrap the RiveAnimation widget with the Opacity widget to reduce the icon’s opacity when it is not selected. The SizedBox widget will help us to define the height and width, ensuring that each icon takes up the same space. Finally, we will wrap everything in a GestureDetector to detect when the user taps on the icon.

Preview with icons

To ensure proper spacing between the icons, we will replace the TODO: Set mainAxisAlignment with the following code:

mainAxisAlignment: MainAxisAlignment.spaceAround
Preview — MainAxisAlignment.spaceAround

Let’s animate our icons when they are tapped. To do this, we need to obtain the input that controls whether the animation should play or stop. In the lib directory, create a new directory called utils and within it, create a file called rive_utils.dart. Create a method called getRiveController that returns the StateMachineController.


import 'package:rive/rive.dart';

class RiveUtils {
static StateMachineController getRiveController(Artboard artboard,
{stateMachineName = "State Machine 1"}) {
StateMachineController? controller =
StateMachineController.fromArtboard(artboard, stateMachineName);
artboard.addController(controller!);
return controller;
}
}

It’s time to call the helper method, insert the following code in place of TODO: Set the input value

StateMachineController controller =
RiveUtils.getRiveController(artboard,
stateMachineName:
bottomNavs[index].stateMachineName);

bottomNavs[index].input =
controller.findSMI("active") as SMIBool;

Replace the TODO: Play animation on tap with the following code.

bottomNavs[index].input!.change(true);
if (bottomNavs[index] != selectedBottomNav) {
setState(() {
selectedBottomNav = bottomNavs[index];
});
}
Future.delayed(const Duration(seconds: 1), () {
bottomNavs[index].input!.change(false);
});

To animate the icons when tapped, we first change the icon’s input value to true, which triggers the animation. Then, we check if the selected bottom tab is the same as the current one. If not, we update the selectedBottomNav to the current tab. The input being set to true will continuously play the icon animation, but we want it to play only once. Since each animation takes 1 second to complete, after 1 second we reset the input value to false.

Preview with animated icon

To enhance the user experience, set the opacity of the unsettled tabs to 0.5. Locate the line TODO: Change Opacity if not selected, then alter the opacity to 1 with the following code.

bottomNavs[index] == selectedBottomNav ? 1 : 0.5

Animated Bar

We’re nearly finished, just need to add the animated indicator on the selected tab. In the lib directory, create a components directory and inside it, create a file named animated_bar.dart

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

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)),
),
);
}
}

Name it AnimatedBar with a boolean property isActive to identify its active state. The AnimatedBar is an AnimatedContainer with a height of 4, an animation duration of 20 milliseconds, and a 2-pixel bottom margin. If it’s active, its width is 20, otherwise it’s 0. Also, add a color and a border radius.

Return to the entry_point.dart and replace the TODO: Animated Bar with the following code.

AnimatedBar(isActive: bottomNavs[index] == selectedBottomNav),
Complete preview

Home Page

We’ve completed the bottom animation, but the screen is blank. Let’s now create the core of any app, the HomePage.

Full Preview of HomePage

In the HomePage, there’s no complex UI, so I won’t go into detail. First, go to the screen directory and create a new directory called home, then inside it, create a file named home_screen.dart

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
bottom: false,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Courses",
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: Colors.black, fontWeight: FontWeight.w600),
),
),
// TODO: Courses with the horizontal scroll

// TODO: Recent courses
],
),
),
),
);
}
}

Let’s create a Course model with parameters for title, description, icon, and background color.

import 'package:flutter/material.dart';

class Course {
final String title, description, iconSrc;
final Color bgColor;

Course({
required this.title,
this.description = "Build and animate an iOS app from scratch",
this.iconSrc = "assets/icons/ios.svg",
this.bgColor = const Color(0xFF7553F6),
});
}

// demo courses list
List<Course> courses = [
Course(title: "Animations in SwiftUI"),
Course(
title: "Animations in Flutter",
iconSrc: "assets/icons/code.svg",
bgColor: const Color(0xFF80A4FF),
),
];

// demo recent courses
List<Course> recentCourses = [
Course(title: "State Machine"),
Course(
title: "Animated Menu",
bgColor: const Color(0xFF9CC5FF),
iconSrc: "assets/icons/code.svg",
),
Course(title: "Flutter with Rive"),
Course(
title: "Animated Menu",
bgColor: const Color(0xFF9CC5FF),
iconSrc: "assets/icons/code.svg",
),
];

In the home directory, create a new directory called components and inside it, create a file named course_card.dart.

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../../../models/course.dart';

class CourseCard extends StatelessWidget {
const CourseCard({
Key? key,
required this.course,
}) : super(key: key);

final Course course;

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
height: 280,
width: 260,
decoration: BoxDecoration(
color: course.bgColor,
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
course.title,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white, fontWeight: FontWeight.w600),
),
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Text(
course.description,
style: const TextStyle(color: Colors.white70),
),
),
const Text(
"61 SECTIONS - 11 HOURS",
style: TextStyle(color: Colors.white54),
),
const Spacer(),
Row(
children: List.generate(
3,
(index) => Transform.translate(
offset: Offset((-10 * index).toDouble(), 0),
child: CircleAvatar(
radius: 20,
backgroundImage: AssetImage(
"assets/avaters/Avatar ${index + 1}.jpg"),
),
),
),
)
],
),
),
SvgPicture.asset(course.iconSrc)
],
),
);
}
}

Replace the TODO: Courses with the horizontal scroll with the following code.

SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...courses
.map((course) => Padding(
padding: const EdgeInsets.only(left: 20),
child: CourseCard(course: course),
))
.toList(),
],
),
),

To add the recent courses, replace the TODO: Recent courses with the following code.

Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Recent",
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(fontWeight: FontWeight.w600),
),
),
...recentCourses.map(
(course) => Padding(
padding:
const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: SecondaryCourseCard(course: course),
),
),

That’s it, we’ve finished our HomePage.👏

Working on the Episode 4…

Episode 4 — Animated Side Menu

I would love to hear your thoughts on this tutorial and how you found it helpful. If you enjoyed it and found it useful, please don’t forget to give it a clap 👏. Your feedback is important to me and helps me to create content that is valuable and useful to you. Thank you for taking the time to read this tutorial, and I hope that it has been helpful in your animation journey with Rive and Flutter.

--

--

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!