DEV Community

Flutter Tanzania
Flutter Tanzania

Posted on

Animated drawer in Flutter

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

Image description

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,
);
Enter fullscreen mode Exit fullscreen mode

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,
),
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

then add translate the transform

Transform(
  transform: Matrix4.identity()
    ..translate(maxSlide)
    ..scale(0.5),
  alignment: Alignment.centerLeft,
  child: myChild,
),
Enter fullscreen mode Exit fullscreen mode

and now our project will look like this

Image description

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 {
Enter fullscreen mode Exit fullscreen mode

then we will declare animation controller just after maxSlide definition

late AnimationController animationController;
Enter fullscreen mode Exit fullscreen mode

then we will initialize it in the init state

@override
void initState() {
  super.initState();
  animationController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 250),
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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,
        ),
      ],
    );
  },
)
Enter fullscreen mode Exit fullscreen mode

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,
          ),
        ],
      );
    },
  ),
)
Enter fullscreen mode Exit fullscreen mode

And with all this our app now will look like this.

Image description

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();
    }
  }
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Oldest comments (0)