People use only 10% of Flutter - Marcin Szalek
When we present flutter to our business clients or bosses we say how easy it is or it is and that we can build cross platform applications with unique customized views and showcase real word examples of flutter applications. However when we start building our flutter projects we end up building apps like this
Well this are not bad or ugly apps and it’s true that they follow material design guideline but we could not list them as beautiful apps and for sure its not taking flutter to its full potential.
In this tutorial we will build a beautiful customized drawer with animation by using only three powerful widgets which are Stack, Transform and AnimationController. While these are common widgets but by applying them we can build beautiful and powerful apps.
Let's see an overview of each widget.
Stack - A widget that positions its children relative to the edges of its box.
This class is useful if you want to overlap several children in a simple way, for example having some text and an image, overlaid with a gradient and a button attached to the bottom.
Read more about stack here
Transform - Transform is a widget that applies a transformation before painting its child, which implies the transformation isn’t considered while figuring how much space this present widget’s child (and accordingly this widget) burns-through.
Read more about transform widget here or here
AnimationController - It's main task is to control animations. The AnimationController class gives you increased flexibility in animation. The animation can be played forward or reverse, and you can stop it.
The AnimationController class produces linear values for a giving duration, and it tries to display a new frame at around 60 frames per second.
So let's start
We will use these three widgets to accomplish building a customized animated drawer.
When we approach designs like this we first need to identify static components. If we look carefully at the design we have two main Widgets which are the main widget and the drawer widget.
After identifying our static widgets we should think of what is happening.
So what’s happening?
- Blue element is behind yellow element
- Yellow element is getting smaller
- Yellow element is moving right
- The transition is smooth
Let’s implement them one by one
Let's start with the blue element behind yellow. Well, this is simple: we create two widgets and put them into a stack.
class CustomDrawer extends StatefulWidget {
const CustomDrawer({super.key});
@override
State<CustomDrawer> createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
myDrawer,
myChild,
],
);
}
}
// drawer widget
Widget myDrawer = Container(
color: Colors.blue,
);
// child widget
Widget myChild = Container(
color: Colors.yellow,
);
To make a child widget get smaller we wrap it to the Transform widget and provide scale to it.
Transform(
transform: Matrix4.identity()..scale(0.5),
alignment: Alignment.centerLeft,
child: myChild,
),
Then to move it to right we will add translate to the transform and our child widget will go to the right.
So start by defining a variable that will carry translate value just after the opening curl brace of the class
double maxSlide = 225.0;
then add translate the transform
Transform(
transform: Matrix4.identity()
..translate(maxSlide)
..scale(0.5),
alignment: Alignment.centerLeft,
child: myChild,
),
and now our project will look like this
And now to make the transition smooth we will add AnimationController widget.
First add SingleTickerProviderStateMixin to our CustomDrawer class
class _CustomDrawerState extends State<CustomDrawer> with SingleTickerProviderStateMixin {
then we will declare animation controller just after maxSlide definition
late AnimationController animationController;
then we will initialize it in the init state
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
}
then we will define a function that will be used to open and close the controller, just add it after the initState.
void toggle() => animationController.isDismissed
? animationController.forward()
: animationController.reverse();
And then in the build method we need to wrap every thing with the AnimatedBuilder widget. So now will return the following in the build method, also we have defined the slide and the scale of our widget.
AnimatedBuilder(
animation: animationController,
builder: (context, _) {
double slide = maxSlide * animationController.value;
double scale = 1 - (animationController.value * 0.3);
return Stack(
children: [
myDrawer,
Transform(
transform: Matrix4.identity()
..translate(slide)
..scale(scale),
alignment: Alignment.centerLeft,
child: myChild,
),
],
);
},
)
And the we will wrap everything in a GestureDetector widget this will help us start and finish the animation. So now our code will look like this.
GestureDetector(
onTap: toggle,
child: AnimatedBuilder(
animation: animationController,
builder: (context, _) {
double slide = maxSlide * animationController.value;
double scale = 1 - (animationController.value * 0.3);
return Stack(
children: [
myDrawer,
Transform(
transform: Matrix4.identity()
..translate(slide)
..scale(scale),
alignment: Alignment.centerLeft,
child: myChild,
),
],
);
},
),
)
And with all this our app now will look like this.
Notice we have not used any complex widget yet, we only used Stack, Transform and AnimationController only.
But we did not achieve our goal yet, because this is not actually the drawer, because drawer is not about only tapping we need to handle the gesture behaviour also to open and close it.
So we can see how flutter team implemented their drawer widget, so to achieve it we will add the following to our GestureDetector widget. First add the following functions and variables.
//
void close() => animationController.reverse();
void open() => animationController.forward();
//
static const double maxSlide = 225.0;
static const double minDragStartEdge = 60;
static const double maxDragStartEdge = maxSlide - 16;
bool _canBeDragged = false;
// on drag start
void _onDragStart(DragStartDetails details) {
bool isDragOpenFromLeft = animationController.isDismissed &&
details.globalPosition.dx < minDragStartEdge;
bool isDragCloseFromRight = animationController.isCompleted &&
details.globalPosition.dx > maxDragStartEdge;
var _canBeDragged = isDragOpenFromLeft || isDragCloseFromRight;
}
void _onDragUpdate(DragUpdateDetails details) {
if (_canBeDragged) {
double delta = details.primaryDelta! / maxSlide;
animationController.value += delta;
}
}
void _onDragEnd(DragEndDetails details) {
if (animationController.isDismissed || animationController.isCompleted) {
return;
}
if (details.velocity.pixelsPerSecond.dx.abs() >= 365.0) {
double visualVelocity = details.velocity.pixelsPerSecond.dx /
MediaQuery.of(context).size.width;
animationController.fling(velocity: visualVelocity);
} else if (animationController.value < 0.5) {
close();
} else {
open();
}
}
So _onDragStart
will be used to determine if we can start opening or closing the drawer, _onDragUpdate
is to determine how big the gesture user did and _onDragEnd
is used to determine if on the end of the dragging behaviour we should open or close or drawer.
And last in our main.dart file add the following code
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: Center(
child: Text('Hello, World!'),
),
drawer: CustomDrawer(),
),
);
}
}
Top comments (0)