Design Complex UI in Flutter - Animated Drawer Widget

Published November 07, 2020

How to implement a complex UI in Flutter.

In this example, we are creating an Animated widget with 3d Animation.

Widgets used in this Example are

  • GestureDetector
  • AnimatedBuilder
  • Transform
  • Stack

 

 

GestureDetector widget is used to Handle the Gesture events like drag events.

AnimatedBuilder widget is used to Make an animation for the widget.

Transform widget is used to make transition animation for the widget

Stack widget is used to align the widgets for the main widget and Drawer events.

 

 

Handle the Drag Events by

GestureDetector(
  onHorizontalDragStart: _onDragStart,
  onHorizontalDragUpdate: _onDragUpdate,
  onHorizontalDragEnd: _onDragEnd,

)

 

Create Animation Controller to do the Animation by

animationController=AnimationController(vsync: this,duration: Duration(milliseconds: 225));

 

Gesture events

void _onDragStart(DragStartDetails dragStartDetails)
{
  bool isDragOpenFromLeft=animationController.isDismissed&&dragStartDetails.globalPosition.dx<minDragStartEdge;
  bool isDragCloseFromRight=animationController.isCompleted&&dragStartDetails.globalPosition.dx>maxDragStartEdge;
  _canBeDragged=isDragOpenFromLeft||isDragCloseFromRight;
}

void _onDragUpdate(DragUpdateDetails dragStartDetails)
{
  if(_canBeDragged)
  {
    double delta=dragStartDetails.primaryDelta/maxSlide;
    animationController.value+=delta;
  }
}
void _onDragEnd(DragEndDetails details) {
  //I have no idea what it means, copied from Drawer
  double _kMinFlingVelocity = 365.0;

  if (animationController.isDismissed || animationController.isCompleted) {
    return;
  }
  if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
    double visualVelocity = details.velocity.pixelsPerSecond.dx /
        MediaQuery.of(context).size.width;

    animationController.fling(velocity: visualVelocity);
  } else if (animationController.value < 0.5) {
    animationController.reverse();
  } else {
    animationController.forward();
  }
}

 

How to add Smooth Animation for the widget?

 

Add Animation for the Main widget by

Transform.translate(
  offset: Offset(maxSlide * animationController.value, 0),
  child: Transform(
    transform: Matrix4.identity()
      ..setEntry(3, 2, 0.001)
      ..rotateY(-math.pi * animationController.value / 2),
    alignment: Alignment.centerLeft,
    child: getWidget(),
  ),
)

 

Add Animation for Drawer widget by

Transform.translate(
  offset: Offset(maxSlide * (animationController.value - 1), 0),
  child: Transform(
    transform: Matrix4.identity()
      ..setEntry(3, 2, 0.001)
      ..rotateY(math.pi / 2 * (1 - animationController.value)),
    alignment: Alignment.centerRight,
    child: MyDrawer(onMenuSelect:(pos){
      animationController.reverse();

      setState(() {

        position=pos;
      });
    }),
  ),
),

 

 

Complete code

main.dart

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

import 'custom_drawer.dart';
import 'home.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Complex UI',
      theme: ThemeData(

        primarySwatch: Colors.blue,

        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: CustomDrawer(),
    );
  }
}

 

CustomDrawer.dart
import 'package:flutter/material.dart';
import 'package:flutter_complex_ui/favourites.dart';
import 'package:flutter_complex_ui/settings.dart';
import 'dart:math' as math;
import 'home.dart';

class CustomDrawer extends StatefulWidget{
  static CustomDrawerState of(BuildContext context) =>
      context.findAncestorStateOfType<CustomDrawerState>();

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return CustomDrawerState();
  }

}
typedef menuCallback = void Function(int pos);
class CustomDrawerState extends State<CustomDrawer> with SingleTickerProviderStateMixin
{
  AnimationController animationController;
  static const double maxSlide = 225;
  static const double minDragStartEdge = 60;
  static const double maxDragStartEdge = maxSlide - 16;
  bool _canBeDragged;
  int position=0;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    animationController=AnimationController(vsync: this,duration: Duration(milliseconds: 225));
  }
  void toogle()
  {
    animationController.isDismissed?animationController.forward():animationController.reverse();
  }
  @override
  Widget build(BuildContext context) {


    return WillPopScope(
      onWillPop: () async {
        if (animationController.isCompleted) {
          close();
          return false;
        }
        return true;
      },
      child: GestureDetector(
        onHorizontalDragStart: _onDragStart,
        onHorizontalDragUpdate: _onDragUpdate,
        onHorizontalDragEnd: _onDragEnd,
        behavior: HitTestBehavior.translucent,
        onTap: animationController.isCompleted ?close () : null,
        child: AnimatedBuilder(
          animation: animationController,
          child: (position==0)?MyHomePage():Settings(),
          builder: (context,_){
            double animValue = animationController.value;
            final slideAmount = maxSlide * animValue;
            final contentScale = 1.0 - (0.3 * animValue);

            return Stack(
              children: [
                Transform.translate(
                  offset: Offset(maxSlide * (animationController.value - 1), 0),
                  child: Transform(
                    transform: Matrix4.identity()
                      ..setEntry(3, 2, 0.001)
                      ..rotateY(math.pi / 2 * (1 - animationController.value)),
                    alignment: Alignment.centerRight,
                    child: MyDrawer(onMenuSelect:(pos){
                      animationController.reverse();

                      setState(() {

                        position=pos;
                      });
                    }),
                  ),
                ),

                Transform.translate(
                  offset: Offset(maxSlide * animationController.value, 0),
                  child: Transform(
                    transform: Matrix4.identity()
                      ..setEntry(3, 2, 0.001)
                      ..rotateY(-math.pi * animationController.value / 2),
                    alignment: Alignment.centerLeft,
                    child: getWidget(),
                  ),
                ),
                Positioned(
                  top: 4.0 + MediaQuery.of(context).padding.top,
                  left: 4.0 + animationController.value * maxSlide,
                  child: Material(
                    color: Colors.transparent,
                    child: IconButton(
                      icon: Icon(Icons.menu),
                      onPressed: toogle,
                    color: Colors.white,
                    ),
                  ),
                ),



              ],
            );
          },
        ),
      ),
    );


  }
  getWidget()
  {
    if(position==0)return MyHomePage();
    else if(position==1)return Favourite();
    else return Settings();
  }


  open()=>animationController.forward();
  close()=>animationController.reverse();

  void setPosition(pos){
    setState(() {
      position=pos;
    });
  }

    void _onDragStart(DragStartDetails dragStartDetails)
    {
      bool isDragOpenFromLeft=animationController.isDismissed&&dragStartDetails.globalPosition.dx<minDragStartEdge;
      bool isDragCloseFromRight=animationController.isCompleted&&dragStartDetails.globalPosition.dx>maxDragStartEdge;
      _canBeDragged=isDragOpenFromLeft||isDragCloseFromRight;
    }

    void _onDragUpdate(DragUpdateDetails dragStartDetails)
    {
      if(_canBeDragged)
      {
        double delta=dragStartDetails.primaryDelta/maxSlide;
        animationController.value+=delta;
      }
    }
    void _onDragEnd(DragEndDetails details) {
      //I have no idea what it means, copied from Drawer
      double _kMinFlingVelocity = 365.0;

      if (animationController.isDismissed || animationController.isCompleted) {
        return;
      }
      if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
        double visualVelocity = details.velocity.pixelsPerSecond.dx /
            MediaQuery.of(context).size.width;

        animationController.fling(velocity: visualVelocity);
      } else if (animationController.value < 0.5) {
        animationController.reverse();
      } else {
        animationController.forward();
      }
    }
    @override
    void dispose() {
      animationController.dispose();
      super.dispose();
    }
  }





class MyDrawer extends StatelessWidget {
  const MyDrawer({this.onMenuSelect});

  final menuCallback onMenuSelect;
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.blueAccent,
      child: SafeArea(
        child: Theme(
          data: ThemeData(brightness: Brightness.dark),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.max,
            children: [
              Container(
                child:  AppBar(title: Text("Menu"), ),
                decoration: new BoxDecoration(
                  gradient: new LinearGradient(
                      colors: [
                        const Color(0xFF3366FF),
                        const Color(0xFF00CCFF),
                      ],
                      begin: const FractionalOffset(0.0, 0.0),
                      end: const FractionalOffset(1.0, 0.0),
                      stops: [0.0, 1.0],
                      tileMode: TileMode.clamp),
                ),
              )
             ,
              /* Image.asset(
                'assets/flutter_europe_white.png',
                width: 200,
              ),*/
              ListTile(
                leading: Icon(Icons.new_releases),
                onTap: (){
                  onMenuSelect(0);
                },
                title: Text('News'),
              ),
              ListTile(
                leading: Icon(Icons.star),
                onTap: (){
                  onMenuSelect(1);
                },
                title: Text('Favourites'),
              ),
              ListTile(
                leading: Icon(Icons.map),
                onTap: (){
                  onMenuSelect(2);
                },
                title: Text('Map'),
              ),
              ListTile(
                leading: Icon(Icons.settings),
                onTap: (){
                  onMenuSelect(3);
                },
                title: Text('Settings'),
              ),
              ListTile(
                leading: Icon(Icons.person),
                onTap: (){
                  onMenuSelect(4);
                },
                title: Text('Profile'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

 

 

Home.dart

import 'package:flutter/material.dart';

import 'custom_drawer.dart';
class MyHomePage extends StatefulWidget {


  MyHomePage({Key key }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List listNews=[];
bool hasPages=true;
ScrollController _scrollController = new ScrollController();
int page=1;
@override
void initState() {

  super.initState();

}
@override
Widget build(BuildContext context) {
  // TODO: implement build
  return Scaffold(
    backgroundColor: Colors.white,
    appBar: AppBar(title: Center(child: Text("News",)), flexibleSpace: Container(
      decoration: new BoxDecoration(
        gradient: new LinearGradient(
            colors: [
              const Color(0xFF3366FF),
              const Color(0xFF00CCFF),
            ],
            begin: const FractionalOffset(0.0, 0.0),
            end: const FractionalOffset(1.0, 0.0),
            stops: [0.0, 1.0],
            tileMode: TileMode.clamp),
      ),
    ),),
    body: ListView(
      children: [
        getMars(Colors.grey),
        getMars(Colors.red),
        getMars(Colors.yellow),
        getMars(Colors.white70),
      ],
    )
  );
}

getMars(color)
{
  return Container(
      height: 120.0,
      margin: const EdgeInsets.symmetric(
        vertical: 16.0,
        horizontal: 24.0,
      ),
      child: new Stack(
        children: <Widget>[
          planetCard(color),
          planetThumbnail,
        ],
      )
  );
}
  final planetThumbnail = new Container(
    margin: new EdgeInsets.symmetric(
        vertical: 16.0
    ),
    alignment: FractionalOffset.centerLeft,
    child:    CircleImage(size:  Size.fromWidth(120),child: Image.network("https://p1.pxfuel.com/preview/366/297/294/model-jewelry-lady-young-indian-girl-royalty-free-thumbnail.jpg",
      width: 100,
      height: 120,
      fit: BoxFit.fitWidth,
    ),),
  );

   planetCard(color) {
     return Container(
       height: 124.0,
       margin: EdgeInsets.only(left: 46.0),
       decoration: BoxDecoration(
         color: (color),
         shape: BoxShape.rectangle,
         borderRadius: BorderRadius.circular(8.0),
         boxShadow: <BoxShadow>[
           BoxShadow(
             color: Colors.black12,
             blurRadius: 10.0,
             offset: Offset(0.0, 10.0),
           ),
         ],
       ),
     );
   }

}

class CircleImage extends StatelessWidget{

  final Size size;
  final Widget child;

  CircleImage({Key key,@required this.child,this.size=const Size.fromWidth(120)}):super(key:key);
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(

      decoration: new BoxDecoration(
          color: Colors.grey,
          shape: BoxShape.circle),
      height: size.width,
      child: ClipOval(
        child: child,

      ),
    );
  }


}

 

 

Settings.dart

import 'package:flutter/material.dart';

class Settings extends StatelessWidget{
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      backgroundColor: Colors.greenAccent,
      appBar: AppBar(title: Center(child: Text("Settings")), flexibleSpace: Container(
        decoration: new BoxDecoration(
          gradient: new LinearGradient(
              colors: [
                const Color(0xFF3366FF),
                const Color(0xFF00CCFF),
              ],
              begin: const FractionalOffset(0.0, 0.0),
              end: const FractionalOffset(1.0, 0.0),
              stops: [0.0, 1.0],
              tileMode: TileMode.clamp),
        ),
      ),),
      body: Container(
        child: Center(
          child: Text("Settings Page"),
        ),
      ),
    );
  }

}

 

Toolbar Searchview in Flutter

 

Integrate RestAPI in Flutter

 

Tags: Drawer Widget, Navigation Drawer, Flutter 3D Animation

 

Article Contributed By :
https://www.rrtutors.com/site_assets/profile/assets/img/avataaars.svg

2941 Views