I really love how Flutter animations can be used to improve usability.

Here’s an example showing a Text widget that shakes when some error occurs.

The Flutter animation APIs make it very easy to implement this. Here’s a step-by-step guide. 👇

1. Create a custom sine curve

The effect is accomplished using an AnimationController and a custom curve based on the sine function.

First of all, here’s a SineCurve that repeats the sine function count times over a 2 * pi period.

// 1. custom Curve subclass
class SineCurve extends Curve {
  SineCurve({this.count = 3});
  final double count;
 
  // 2. override transformInternal() method
  @override
  double transformInternal(double t) {
    return sin(count * 2 * pi * t);
  }
}

Since SineCurve is a Curve subclass, it can be given as an argument to any implicitly animated widget.

2. Abstract the AnimationController boilerplate code

We’ll need an AnimationController to get the effect we want. To reduce the boilerplate code, let’s define a State subclass:

abstract class AnimationControllerState<T extends StatefulWidget>
    extends State<T> with SingleTickerProviderStateMixin {
  AnimationControllerState(this.animationDuration);
  final Duration animationDuration;
  late final animationController = AnimationController(
    vsync: this,
    duration: animationDuration
  );
 
  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }
}

For more details about this technique, see: How to reduce AnimationController boilerplate code: Flutter Hooks vs extending the State class

3. Create a custom ShakeWidget

Let’s define a StatefulWidget subclass that takes a child widget along with some customizable properties:

class ShakeWidget extends StatefulWidget {
  const ShakeWidget({
    Key? key,
    required this.child,
    required this.shakeOffset,
    this.shakeCount = 3,
    this.shakeDuration = const Duration(milliseconds: 500),
  }) : super(key: key);
  // 1. pass a child widget
  final Widget child;
  // 2. configurable properties
  final double shakeOffset;
  final int shakeCount;
  final Duration shakeDuration;
 
  // 3. pass the shakeDuration as an argument to ShakeWidgetState. See below.
  @override
  ShakeWidgetState createState() => ShakeWidgetState(shakeDuration);
}

4. Create a custom CurvedAnimation

Let’s define the ShakeWidgetState class with a custom animation:

// note: ShakeWidgetState is public
class ShakeWidgetState extends AnimationControllerState<ShakeWidget> {
  ShakeWidgetState(Duration duration) : super(duration);
  // 1. create a Tween
  late Animation<double> _sineAnimation = Tween(
    begin: 0.0,
    end: 1.0,
    // 2. animate it with a CurvedAnimation
  ).animate(CurvedAnimation(
    parent: animationController,
    // 3. use our SineCurve
    curve: SineCurve(count: widget.shakeCount.toDouble()),
  ));
}

5. Use the animation with AnimatedBuilder and Transform.translate

Let’s define a build() method with a custom AnimatedBuilder:

@override
Widget build(BuildContext context) {
  // 1. return an AnimatedBuilder
  return AnimatedBuilder(
    // 2. pass our custom animation as an argument
    animation: _sineAnimation,
    // 3. optimization: pass the given child as an argument
    child: widget.child,
    builder: (context, child) {
      return Transform.translate(
        // 4. apply a translation as a function of the animation value
        offset: Offset(_sineAnimation.value * widget.shakeOffset, 0),
        // 5. use the child widget
        child: child,
      );
    },
  );
}

Note how we’re passing _sineAnimation as an argument to the AnimatedBuilder, and also use it to calculate the offset value. See below for an alternative approach.

6. Add a status listener to reset the animation when complete

As we want to “play” the animation many times, we need to reset the AnimationController when the animation is complete:

@override
void initState() {
  super.initState();
  // 1. register a status listener
  animationController.addStatusListener(_updateStatus);
}
 
@override
void dispose() {
  // 2. dispose it when done
  animationController.removeStatusListener(_updateStatus);
  super.dispose();
}
 
void _updateStatus(AnimationStatus status) {
  // 3. reset animationController when the animation is complete
  if (status == AnimationStatus.completed) {
    animationController.reset();
  }
}

7. Add a shake() method

Our ShakeWidgetState class needs a shake() method that we can call from outside to start the animation:

// note: this method is public
void shake() {
  animationController.forward();
}

8. Control the ShakeWidget with a GlobalKey

In the parent widget we can declare a GlobalKey<ShakeWidgetState>, and use it to call shake() when a button is pressed.

class MyHomePage extends StatelessWidget {
  // 1. declare a GlobalKey
  final _shakeKey = GlobalKey<ShakeWidgetState>();
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 2. shake the widget via the GlobalKey when a button is pressed
        ElevatedButton(
          child: Text('Sign In', style: TextStyle(fontSize: 20)),
          onPressed: () => _shakeKey.currentState?.shake(),
        ),
        // 3. Add a parent ShakeWidget to the child widget we want to animate
        ShakeWidget(
          // 4. pass the GlobalKey as an argument
          key: _shakeKey,
          // 5. configure the animation parameters
          shakeCount: 3,
          shakeOffset: 10,
          shakeDuration: Duration(milliseconds: 400),
          // 6. Add the child widget that will be animated
          child: Text(
            'Invalid credentials',
            textAlign: TextAlign.center,
            style: TextStyle(
                color: Colors.red, fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
      ],
    );
  }
}

Here’s the final result (with a slightly more interesting UI):

Final Notes: implicit vs explicit animations

The SineCurve class we created is a Curve subclass, so it can be given as an argument to any implicitly animated widget.

In this example, we used it to create a custom CurvedAnimation that is passed as an argument to our AnimatedBuilder.

But since we’re using an explicit animation, we don’t need the SineCurve or even the _sineAnimation to start with. In fact, we can get the same result by calculating the sine value directly inside the AnimatedBuilder code:

@override
Widget build(BuildContext context) {
  // 1. return an AnimatedBuilder
  return AnimatedBuilder(
    // 2. pass the AnimationController as an argument
    animation: animationController,
    // 3. optimization: pass the given child as an argument
    child: widget.child,
    builder: (context, child) {
      // 4. calculate the sine value directly
      final sineValue =
          sin(widget.shakeCount * 2 * pi * animationController.value);
      return Transform.translate(
        // 5. apply a translation as a function of the animation value
        offset: Offset(sineValue * widget.shakeOffset, 0),
        // 6. use the child widget
        child: child,
      );
    },
  );
}

Here’s the full source code for this example:

That’s it. Now go ahead and shake your widgets! 😎

Happy coding!