Search
  • Alex Fourman

How to - "dismiss up" Flutter animation

Recently I needed a to create an animation to dismiss a banner so that it slides up under the app bar. I was sure that using SlideTransition is going to be the end of it but I was quite surprised that it keeps the original space of the widget as it was at the start of the animation. So I kept looking, trying Transform and FractionalTranslation, couldn’t make neither work. So I looked into the code of something similar from the flutter framework, the ExpansionTile, and that is where I got the inspiration from.


This is the starting point

I always try my code in a side project when I want to have less distractions.


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: <Widget>[
          MaterialBanner(
            content: Text('This is a banner. Dismiss me'),
            actions: <Widget>[
              FlatButton(
                onPressed: () {},
                child: Text('DISMISS'),
              ),
            ],
          ),
          Container(
            height: 200,
            alignment: Alignment.center,
            color: Colors.blue,
            child: Text('A Container'),
          ),
        ],
      ),
    );
  }
}

Now, with animation


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

// We add the [SingleTickerProviderStateMixin] because it allows us to 
// create an
//[AnimationController] within the class
class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin<MyHomePage> {
  ///The animation controller that starts the slide effect
  AnimationController _slideAnimationController;

  ///The animation that creates the slide up effect by controlling the 
  // height
  ///factor of the [Align] widget
  Animation<double> _heightFactorAnimation;

  @override
  void initState() {
    _slideAnimationController = AnimationController(
      vsync: this,
      //whatever duration you want
      duration: Duration(milliseconds: 400),
    );
    _heightFactorAnimation = CurvedAnimation(
        parent: _slideAnimationController.drive(
          // a Tween from 1.0 to 0.0 is what makes the slide effect by 
          // shrinking
          // the container using the [Align.heightFactor] parameter
          Tween<double>(
            begin: 1.0,
            end: 0.0,
          ),
        ),
        curve: Curves.ease);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: <Widget>[
          //This is where the magic begins
          AnimatedBuilder(
            animation: _slideAnimationController,
            builder: (BuildContext context, Widget child) {
              return ClipRect(
                child: Align(
                  alignment: Alignment.bottomCenter,
                  heightFactor: _heightFactorAnimation.value,
                  child: child,
                ),
              );
            },
            // This is where the magic ends. Just so we're clear

            // we wrap our content in the child param as it is 
            // not affected by
            // the animation itself
            child: MaterialBanner(
              content: Text('This is a banner. Dismiss me'),
              actions: <Widget>[
                FlatButton(
                  onPressed: () {
                    //pressing the button will start the animation
                    _slideAnimationController.forward();
                  },
                  child: Text('DISMISS'),
                ),
              ],
            ),
          ),
          Container(
            height: 200,
            alignment: Alignment.center,
            color: Colors.blue,
            child: Text('A Container'),
          ),
        ],
      ),
    );
  }
}

If you look closer you can see that while visually the widget has been dismissed it is still there in the widget tree after the animation finishes. To achieve that we need an extra step and here to each his own, some people would use setState() I personally prefer having a model and for my widget to listen to it. So I wrap the widget with ValueListenableBuilder and Visibility.


Here is the final result

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

//We add the [SingleTickerProviderStateMixin] because it allows us to 
//create an
//[AnimationController] within the class
class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin<MyHomePage> {
  ///The animation controller that starts the slide effect
  AnimationController _slideAnimationController;

  ///The animation that creates the slide up effect by controlling the 
  //height
  ///factor of the [Align] widget
  Animation<double> _heightFactorAnimation;

  ///Governs whether to show the banner or not. We use a [ValueNotifier]
  ///because the visibility changes asynchronously when the animation 
  //finishes,
  ///which we want to trigger the rebuild of [ValueListenableBuilder] That
  ///listens to this value
  ValueNotifier<bool> _isVisibleValueNotifier = ValueNotifier(true);

  @override
  void initState() {
    _slideAnimationController = AnimationController(
      vsync: this,
      //whatever duration you want
      duration: Duration(milliseconds: 400),
    );
    _heightFactorAnimation = CurvedAnimation(
        parent: _slideAnimationController.drive(
          //a Tween from 1.0 to 0.0 is what makes the slide effect by 
          //shrinking
          // the container using the [Align.heightFactor] parameter
          Tween<double>(
            begin: 1.0,
            end: 0.0,
          ),
        ),
        curve: Curves.ease);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: <Widget>[
          //This is where the magic begins
          ValueListenableBuilder<bool>(
            valueListenable: _isVisibleValueNotifier,
            builder: (context, isVisible, child) {
              return Visibility(
                visible: isVisible,
                child: child,
              );
            },
            child: AnimatedBuilder(
              animation: _slideAnimationController,
              builder: (BuildContext context, Widget child) {
                return ClipRect(
                  child: Align(
                    alignment: Alignment.bottomCenter,
                    heightFactor: _heightFactorAnimation.value,
                    child: child,
                  ),
                );
              },
              //This is where the magic ends. Just so we're clear

              // we wrap our content in the child param as it is not 
              // affected by
              // the animation itself
              child: MaterialBanner(
                content: Text('This is a banner. Dismiss me'),
                actions: <Widget>[
                  FlatButton(
                    onPressed: () async {
                      //pressing the button will start the animation
                      await _slideAnimationController.forward();
                      _isVisibleValueNotifier.value = false;
                    },
                    child: Text('DISMISS'),
                  ),
                ],
              ),
            ),
          ),
          Container(
            height: 200,
            alignment: Alignment.center,
            color: Colors.blue,
            child: Text('A Container'),
          ),
        ],
      ),
    );
  }
}

9 views0 comments

Recent Posts

See All