In this article, I am going to introduce a side menu drawer using Rive animation. Along with learning the awesome 3D side menu drawer using rive animation UI implementation in Flutter, we will also learn how its coding workflows and structures work.
What is Rive?
Rive is a very useful animation tool that can create beautiful animations and we can add these to our Application. In Flutter, we can add animations by writing so many lines of code but this is not a good practice for a developer. Instead of writing lines of code to create animation, we can create one using this powerful Rive animation tool. Please read all the below points in sequence to understand the topic clearly.
How to Create a New Future Project
First, we need to create a new Flutter project. For that, make sure that you've installed the Flutter SDK and other Flutter app development-related requirements.
If everything is properly set up, then in order to create a project we can simply run the following command in our desired local directory:
flutter create flutter-sidemenu-rive
After we've set up the project, we can navigate inside the project directory and execute the following command in the terminal to run the project in either an available emulator or an actual device:
flutter run
First, we need to make some simple configurations to the default boilerplate code in the main.dart file. We'll remove some default code and add the simple MaterialApp
pointing to the remove Container
and Call NavigationPoint page for now:
import 'package:flutter/material.dart';
import 'package:riveanimation/navigationPoint.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Rive Drawer and bottom tabs devhubspot',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: NavigationPoint()
);
}
}
After Creating the utils folder inner lib folder and create a file riveutils.dart and add the following code
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;
}
}
After Creating the models folder inner lib folder and create a file riveassets.dart and add the following code
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;
}
}
List<RiveAsset> sideMenus = [
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "HOME",
stateMachineName: "HOME_Interactivity",
title: "Home",
),
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "SEARCH",
stateMachineName: "SEARCH_Interactivity",
title: "Search",
),
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "LIKE/STAR",
stateMachineName: "STAR_Interactivity",
title: "Favorites",
),
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "CHAT",
stateMachineName: "CHAT_Interactivity",
title: "Help",
),
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "TIMER",
stateMachineName: "TIMER_Interactivity",
title: "History",
),
RiveAsset(
"assets/RiveAssets/icons.riv",
artboard: "BELL",
stateMachineName: "BELL_Interactivity",
title: "Notification",
),
];
After Creating the sidemenu folder inner lib folder and creating a file sidemenu.dart widget and add the following code
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:riveanimation/models/riveassets.dart';
import 'package:riveanimation/utils/riveutils.dart';
class SideMenu extends StatefulWidget {
const SideMenu({super.key});
@override
State<SideMenu> createState() => _SideMenuState();
}
class _SideMenuState extends State<SideMenu> {
RiveAsset selectedMenu = sideMenus.first;
@override
Widget build(BuildContext context) {
StateMachineController controller;
return Scaffold(
body: Container(
width: 288,
height: double.infinity,
color: Color(0xff17203a),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.white24,
child: Icon(Icons.person, color: Colors.white),
),
title: Text("DevHubSpot", style: TextStyle(color: Colors.white)),
subtitle: Text("Blogger", style: TextStyle(color: Colors.white),),
),
Padding(
padding: EdgeInsets.only(left: 22, top:30, bottom: 14),
child: Text("Browser", style: TextStyle(color: Colors.white),),
),
...sideMenus.map(
(menu) => Column(
children: [
Padding(
padding: EdgeInsets.only(left: 22),
child: Divider(
color: Colors.white24,
height: 1,
),
),
Stack(
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
height: 56,
width: selectedMenu == menu ? 288:0,
left: 0,
child: Container(
decoration: BoxDecoration(
color: Color(0xff6792ff),
borderRadius: BorderRadius.all(Radius.circular(10))
),
),
),
ListTile(
onTap: () {
print(menu);
menu.input?.change(true);
setState(() {
selectedMenu = menu;
});
Future.delayed(const Duration(seconds: 1), () {
menu.input?.change(false);
});
},
leading: SizedBox(
height: 35, width: 35,
child: RiveAnimation.asset(
menu.src,
artboard: menu.artboard,
onInit: (artboard) {
StateMachineController controller = RiveUtils.getRiveController(artboard, stateMachineName: menu.stateMachineName);
menu.input = controller.findSMI("active") as SMIBool;
},
),
),
title : Text(menu.title, style: TextStyle(color: Colors.white),)
)
],
)
],
)
)
],
),
),
),
);
}
}
After Creating the models folder inner lib folder and create a file instructor.dart and add the following code
import 'package:flutter/material.dart';
class Instructor{
final String title, description, iconSrc;
final Color bgColor;
Instructor({required this.title, this.description = "Build animate flutter app from scratch", this.iconSrc = "assets/icons/2.jpg", this.bgColor = const Color(0xff7553F6)});
}
List<Instructor> instructors = [
Instructor(title: "Flutter animation app"),
Instructor(title: "Devhubspot Flutter tutorial", bgColor:Colors.blueAccent),
Instructor(title: "Devhubspot Flutter rive animation", bgColor:Colors.deepPurpleAccent),
Instructor(title: "Devhubspot Flutter animation", bgColor:Colors.pinkAccent),
];
After Creating the screens folder inner lib folder and create a file home.dart and add the following code
import 'package:flutter/material.dart';
import 'package:riveanimation/models/instructor.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
body: SafeArea(
bottom: false,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40,),
Padding(padding: EdgeInsets.only(top: 40, bottom: 10, left: 15),
child: Text("Instructor", style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600)),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...instructors.map((e) => Padding(
padding: EdgeInsets.all(10),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 22),
height: 200,
width: 260,
decoration: BoxDecoration(
color: e.bgColor,
borderRadius: BorderRadius.all(Radius.circular(20))
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.title, style: TextStyle(color:Colors.white, fontWeight: FontWeight.w600)),
Padding(padding: EdgeInsets.only(top: 12, bottom: 8),
child: Text(e.description, style: TextStyle(color: Colors.white70))),
],
))
],
),
),
)).toList(),
],
),
),
Padding(padding: EdgeInsets.only(top: 40, bottom: 10, left: 15),
child: Text("Recent Instructor", style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600)),
),
Container(
child: Column(
children: [
...instructors.map((e) => Padding(
padding: EdgeInsets.all(10),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 22),
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: e.bgColor,
borderRadius: BorderRadius.all(Radius.circular(20))
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.title, style: TextStyle(color:Colors.white, fontWeight: FontWeight.w600)),
Padding(padding: EdgeInsets.only(top: 12, bottom: 8),
child: Text(e.description, style: TextStyle(color: Colors.white70))),
],
))
],
),
),
)).toList(),
],
),
)
],
),
),
),
);
}
}
After Creating NavigationPoint.dart file in the inner lib folder
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:riveanimation/screens/home.dart';
import 'package:riveanimation/sidemenu/sidemenu.dart';
import 'package:rive/rive.dart';
import 'package:riveanimation/utils/riveutils.dart';
class NavigationPoint extends StatefulWidget {
const NavigationPoint({super.key});
@override
State<NavigationPoint> createState() => _NavigationPointState();
}
class _NavigationPointState extends State<NavigationPoint> with SingleTickerProviderStateMixin{
late AnimationController _animationController;
late Animation<double> animation;
late Animation<double> scalAnimation;
late SMIBool isSideBarClosed;
bool isSideMenuClosed = true;
@override
void initState() {
// TODO: implement initState
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
)..addListener(() {
setState(() {});
});
animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _animationController
, curve: Curves.fastOutSlowIn),
);
scalAnimation = Tween<double>(begin: 1, end: 0.8).animate(
CurvedAnimation(parent: _animationController
, curve: Curves.fastOutSlowIn),
);
super.initState();
}
@override
void dispose() {
// TODO: implement dispose
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
extendBody: true,
body: Stack(
// It's time to add the SideMenu
children: [
// It shows nothing
// because now it's under the HomeScreen
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
curve: Curves.fastOutSlowIn,
width: 288,
left: isSideMenuClosed ? -288 : 0,
height: MediaQuery.of(context).size.height,
child: const SideMenu(),
),
Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(animation.value - 40 * animation.value * pi / 180),
child: Transform.translate(
offset: Offset(animation.value * 265, 0),
child: Transform.scale(
scale: scalAnimation.value,
child: const ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(24)),
child: Home(),
),
),
),
),
// As you can see it's an ANimated button
AnimatedPositioned(
duration: Duration(milliseconds: 200),
curve: Curves.fastOutSlowIn,
left: isSideMenuClosed ? 0 : 220,
top: 16,
child: SafeArea(
child: GestureDetector(
onTap: (){
isSideBarClosed.value = !isSideBarClosed.value;
if (isSideMenuClosed) {
_animationController.forward();
} else {
_animationController.reverse();
}
setState(() {
isSideMenuClosed = isSideBarClosed.value;
});
},
child: Container(
margin: const EdgeInsets.only(left: 16),
height: 40,
width: 40,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(0, 3),
blurRadius: 8,
)
],
),
child: RiveAnimation.asset(
"assets/RiveAssets/menu_button.riv",
onInit: (artboard) {
StateMachineController controller = RiveUtils.getRiveController(
artboard,
stateMachineName: "State Machine");
isSideBarClosed = controller.findSMI("isOpen") as SMIBool;
// Now it's easy to understand
isSideBarClosed.value = true;
},
),
),
),
)
),
],
),
);
}
}
Output: