Skip to content

Create a Bouncing Button Animation in Flutter

| 5 min read

In Flutter creating animations is very simple and intuitive. In this case you will learn how to create a bouncing button animation working with controllers and values.

The final result of the tutorial

Bouncing Button Preview

AnimationController class lets you perform tasks such as:

AnimationController produces values that range from 0.0 to 1.0. during a given duration.

Ticker providers

An AnimationController needs a TickerProvider, which is configured using the vsync argument on the constructor.

The TickerProvider interface describes a factory for Ticker objects. A Ticker is an object that knows how to register itself with the SchedulerBinding and fires a callback every frame.

The AnimationController class uses a Ticker to step through the animation that it controls.

If an AnimationController is being created from a State, then the State can use the TickerProviderStateMixin and SingleTickerProviderStateMixin classes to implement the TickerProvider interface.

The TickerProviderStateMixin class always works for this purpose; the SingleTickerProviderStateMixin is slightly more efficient in the case of the class only ever needing one Ticker.

AnimationWidget

class AnimatedButton extends AnimatedWidget {
  final AnimationController _controller;
  const AnimatedButton({
    @required AnimationController controller,
  })  : _controller = controller,
        super(listenable: controller); // (a).

  @override
  Widget build(BuildContext context) {
    return Transform.scale( // (b).
      scale: 1 - _controller.value, // (c).
      child: Container(
        height: 70,
        width: 200,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20.0),
            boxShadow: const [
              BoxShadow(
                color: Color(0x80000000),
                blurRadius: 10.0,
                offset: Offset(0.0, 2.0),
              ),
            ],
            gradient: LinearGradient(
              colors: const [
                Color(0xff00e6dc),
                Color(0xff00ffb9),
              ],
            )),
        child: const Center(
          child: Text('Press button',
              style: TextStyle(
                fontSize: 20.0,
                fontWeight: FontWeight.bold,
                color: Color(0xff000028),
              )),
        ),
      ),
    );
  }
}

The layout button is based on a Container with a text widget. The button has border radius and a linear gradient style.

Bouncing Button Widget

class BouncingButton extends StatefulWidget {
  @override
  _BouncingButtonState createState() => _BouncingButtonState();
}

class _BouncingButtonState extends State<BouncingButton>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController( // (a).
      vsync: this, // (b).
      duration: Duration(
        milliseconds: 500,
      ),
      lowerBound: 0.0,
      upperBound: 0.1,
    )..addListener(() { // (c).
        setState(() {});
      });
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    // (d).
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Bouncing Button Animation',
            style: TextStyle(color: Colors.grey[700], fontSize: 20.0),
          ),
          const SizedBox(
            height: 20.0,
          ),
          Center(
            child: GestureDetector(
              onTapDown: _tapDown,
              onTapUp: _tapUp,
              child: AnimatedButton(controller: _controller),
            ),
          ),
        ],
      ),
    );
  }

  // (e).
  void _tapDown(TapDownDetails details) {
    _controller.forward();
  }

  // (f).
  void _tapUp(TapUpDetails details) {
    _controller.reverse();
  }
}

The full code

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: BouncingButton(),
    );
  }
}

class BouncingButton extends StatefulWidget {
  @override
  _BouncingButtonState createState() => _BouncingButtonState();
}

class _BouncingButtonState extends State<BouncingButton>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(
        milliseconds: 500,
      ),
      lowerBound: 0.0,
      upperBound: 0.1,
    )..addListener(() {
        setState(() {});
      });
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Bouncing Button Animation',
            style: TextStyle(color: Colors.grey[700], fontSize: 20.0),
          ),
          const SizedBox(
            height: 20.0,
          ),
          Center(
            child: GestureDetector(
              onTapDown: _tapDown,
              onTapUp: _tapUp,
              child: AnimatedButton(controller: _controller),
            ),
          ),
        ],
      ),
    );
  }

  void _tapDown(TapDownDetails details) {
    _controller.forward();
  }

  void _tapUp(TapUpDetails details) {
    _controller.reverse();
  }
}

class AnimatedButton extends AnimatedWidget {
  final AnimationController _controller;
  const AnimatedButton({
    @required AnimationController controller,
  })  : _controller = controller,
        super(listenable: controller);

  @override
  Widget build(BuildContext context) {
    return Transform.scale(
      scale: 1 - _controller.value,
      child: Container(
        height: 70,
        width: 200,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20.0),
            boxShadow: const [
              BoxShadow(
                color: Color(0x80000000),
                blurRadius: 10.0,
                offset: Offset(0.0, 2.0),
              ),
            ],
            gradient: LinearGradient(
              colors: [
                Color(0xff00e6dc),
                Color(0xff00ffb9),
              ],
            )),
        child: const Center(
          child: Text('Press button',
              style: TextStyle(
                fontSize: 20.0,
                fontWeight: FontWeight.bold,
                color: Color(0xff000028),
              )),
        ),
      ),
    );
  }
}

I hope that this tip will help you for your next mobile development.

See you in the next tutorial. 😉