Search code examples
flutterdartflutter-layoutflutter-animationflutter-alertdialog

How to make Alert Dialog shaking animation in Flutter


I want to shake the alert dialog like this: (not only the text, but also the entire pop up dialog)

https://www.youtube.com/watch?v=IaHMoifUBSw

How can I shake the whole alert dialog when user clicks a button?


Solution

  • This can be done with AnimatedBuilder and Transform widgets. Use sin function from dart:math to map the AnimationController value between 0.0 to 1.0 into a smooth sinusoidal wave with desired amplitude. The period can be specified directly using duration in the AnimationController itself.

    To start the animation, you can either call controller.repeat() to make it run indefinitely, until you call controller.stop(), or you can use controller.forward() to run it once.

    To let it shake 3 times then stop, for example, you can do this:

    onPressed: () async {
      await _controller.forward(from: 0.0);
      await _controller.forward(from: 0.0);
      await _controller.forward(from: 0.0);
    },
    

    This is how it looks in action (please bare with the frame rate limit of GIF):

    demo gif

    Full source code for you to use as a starting point is attached below. You can adjust duration and distance to change the intensity of the shaking animation:

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Test(),
        );
      }
    }
    
    class Test extends StatelessWidget {
      const Test({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Shaking Dialog Demo')),
          body: Center(
            child: ElevatedButton(
              child: Text('Show Dialog'),
              onPressed: () {
                showDialog(
                  context: context,
                  builder: (_) => ShakeableDialog(),
                );
              },
            ),
          ),
        );
      }
    }
    
    class ShakeableDialog extends StatefulWidget {
      final Duration duration; // how fast to shake
      final double distance; // how far to shake
    
      const ShakeableDialog({
        Key? key,
        this.duration = const Duration(milliseconds: 300),
        this.distance = 24.0,
      }) : super(key: key);
    
      @override
      _ShakeableDialogState createState() => _ShakeableDialogState();
    }
    
    class _ShakeableDialogState extends State<ShakeableDialog>
        with SingleTickerProviderStateMixin {
      late final _controller = AnimationController(
        vsync: this,
        duration: widget.duration,
      );
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            final dx = sin(_controller.value * 2 * pi) * widget.distance;
            return Transform.translate(
              offset: Offset(dx, 0),
              child: child,
            );
          },
          child: AlertDialog(
            title: Text('Alert Dialog Title'),
            content: Text('Try these buttons!'),
            actions: [
              TextButton(
                child: Text('SHAKE 3 TIMES'),
                onPressed: () async {
                  await _controller.forward(from: 0.0);
                  await _controller.forward(from: 0.0);
                  await _controller.forward(from: 0.0);
                },
              ),
              TextButton(
                child: Text('KEEP SHAKING'),
                onPressed: () => _controller.repeat(),
              ),
              TextButton(
                child: Text('STOP SHAKING'),
                onPressed: () => _controller.stop(),
              ),
              TextButton(
                child: Text('CLOSE'),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ],
          ),
        );
      }
    }