How to shake a widget in Flutter on invalid input?
Asked Answered
R

9

9

On my signup form, I have a checkbox which needs to shake a bit whenever the user tries to login before accepting the terms and conditions. How can I achieve something like this Flutter?

Ranita answered 1/12, 2019 at 7:28 Comment(2)
see SlideTransition (or AlignTransition)Orgy
you can use Transform too but its a little bit complexOrgy
M
12
import 'package:flutter/material.dart';

@immutable
class ShakeWidget extends StatelessWidget {
  final Duration duration;
  final double deltaX;
  final Widget child;
  final Curve curve;

  const ShakeWidget({
    Key key,
    this.duration = const Duration(milliseconds: 500),
    this.deltaX = 20,
    this.curve = Curves.bounceOut,
    this.child,
  }) : super(key: key);

  /// convert 0-1 to 0-1-0
  double shake(double animation) =>
      2 * (0.5 - (0.5 - curve.transform(animation)).abs());

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder<double>(
      key: key,
      tween: Tween(begin: 0.0, end: 1.0),
      duration: duration,
      builder: (context, animation, child) => Transform.translate(
        offset: Offset(deltaX * shake(animation), 0),
        child: child,
      ),
      child: child,
    );
  }
}

If you need to re-enable shaking, just change ShakeWidget key to some random one.

Malocclusion answered 5/6, 2020 at 9:59 Comment(6)
how does this answer the question? i understand that this shakes the widget, but it cannot be applied on invalid input.Chilopod
shakeWidget.key = UniqueKey(); is giving error 'key' can't be used as a setter because it's final.Division
how to repeat the shake animation in loop?Division
I found it better using : double shake(double animation) => sin(animation * pi * 4);Extinction
works great. but is there any other way to trigger the shake effect other than changing the key?Groundhog
@NehalJaisalmeria : check TurboCoding 's answerSidran
C
5

I achieved this a different way because I wanted to be able to control the duration and get a bit more vigorous shaking. I also wanted to be able to add this easily as a wrapper for other child widgets so I looked up how to use keys to have a parent control actions in a child widget. Here is the class:

class ShakerState extends State<Shaker>   with SingleTickerProviderStateMixin {
  late AnimationController animationController;
  late Animation<double> animation;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 800), // how long the shake happens
    )..addListener(() => setState(() {}));

    animation = Tween<double>(
      begin: 00.0,
      end: 120.0,
    ).animate(animationController);
  }

  math.Vector3 _shake() {
    double progress = animationController.value;
    double offset = sin(progress * pi * 10.0);  // change 10 to make it vibrate faster
    return math.Vector3(offset * 25, 0.0, 0.0);  // change 25 to make it vibrate wider
  }

  shake() {
    animationController.forward(from:0);
  }

  @override
  Widget build(BuildContext context) {
    return Transform(
        transform: Matrix4.translation(_shake()),
        child: widget.child,
      );
  }
}

And then to use this you need a key in your parent:

  final GlobalKey<ShakerState> _shakeKey = GlobalKey<ShakerState>();

And then you can do something like this inside your parent body (see where "Shaker" is used around the child I want to shake):

    ...
    Container(
      height: 50,
      width: 250,
      decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(20)),
      child: TextButton(
        onPressed: () => _handleEmailSignIn(loginController.text, loginPasswordController.text),
        child: Shaker(_shakeKey, Text('Login',   // <<================
          style: TextStyle(color: Colors.white, fontSize: 25),
        )),
      ),
    ),
    ...

Then with the controller you can trigger the shake at a time you want programmatically like this (see the use of "_shakeKey"):

  Future<void> _handleEmailSignIn(String user, password) async {
    try {
      await auth.signInWithEmailAndPassword(email: user, password: password);
      FocusScope.of(context).unfocus();
      await Navigator.pushNamedAndRemoveUntil(context, '/next_page',  ModalRoute.withName('/'));
    } on FirebaseAuthException catch (e) {

      _shakeKey.currentState?.shake(); // <<=============

      if (e.code == 'user-not-found') {
        print('No user found for that email.');
      } else if (e.code == 'wrong-password') {
        print('Wrong password provided for that user.');
      }
    }
    setState(() {});
  }
Coeliac answered 7/4, 2021 at 20:59 Comment(2)
how to import math?Suave
@BurakMeteErdoğan he is using the [vector math] (pub.dev/packages/vector_math library), and writing import 'package:vector_math/vector_math_64.dart' as math;Donoho
B
3

Little improved of @Kent code (added controller).

import 'package:flutter/material.dart';

class ShakeX extends StatefulWidget {
  final Widget child;
  final double horizontalPadding;
  final double animationRange;
  final ShakeXController controller;
  final Duration animationDuration;

  const ShakeX(
      {Key key,
      @required this.child,
      this.horizontalPadding = 30,
      this.animationRange = 24,
      this.controller,
      this.animationDuration = const Duration(milliseconds: 500)})
      : super(key: key);

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

class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
  AnimationController animationController;

  @override
  void initState() {
    animationController =
        AnimationController(duration: widget.animationDuration, vsync: this);

    if (widget.controller != null) {
      widget.controller.setState(this);
    }

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final Animation<double> offsetAnimation =
        Tween(begin: 0.0, end: widget.animationRange)
            .chain(CurveTween(curve: Curves.elasticIn))
            .animate(animationController)
              ..addStatusListener((status) {
                if (status == AnimationStatus.completed) {
                  animationController.reverse();
                }
              });

    return AnimatedBuilder(
      animation: offsetAnimation,
      builder: (context, child) {
        return Container(
          margin: EdgeInsets.symmetric(horizontal: widget.animationRange),
          padding: EdgeInsets.only(
              left: offsetAnimation.value + widget.horizontalPadding,
              right: widget.horizontalPadding - offsetAnimation.value),
          child: widget.child,
        );
      },
    );
  }
}

class ShakeXController {
  _ShakeXState _state;
  void setState(_ShakeXState state) {
    _state = state;
  }

  Future<void> shake() {
    print('shake');
    return _state.animationController.forward(from: 0.0);
  }
}
Beria answered 3/12, 2020 at 7:54 Comment(1)
Is there an easy modification to make it bound maybe a few more times?Coeliac
B
3
import 'package:flutter/material.dart';

class ShakeError extends StatefulWidget {
  const ShakeError({
    Key? key,
    required this.child,
    this.controller,
    this.duration = const Duration(milliseconds: 500),
    this.deltaX = 20,
    this.curve = Curves.bounceOut,
  }) : super(key: key);
  final Widget child;
  final Duration duration;
  final double deltaX;
  final Curve curve;
  final Function(AnimationController)? controller;

  @override
  State<ShakeError> createState() => _ShakeErrorState();
}

class _ShakeErrorState extends State<ShakeError>
    with SingleTickerProviderStateMixin<ShakeError> {
  late AnimationController controller;
  late Animation<double> offsetAnimation;

  @override
  void initState() {
    controller = AnimationController(duration: widget.duration, vsync: this);
    offsetAnimation = Tween<double>(begin: 0.0, end: 1.0)
        .chain(CurveTween(curve: widget.curve))
        .animate(controller);
    if (widget.controller is Function) {
      widget.controller!(controller);
    }
    super.initState();
  }

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

  /// convert 0-1 to 0-1-0
  double shake(double animation) =>
      2 * (0.5 - (0.5 - widget.curve.transform(animation)).abs());

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: offsetAnimation,
      builder: (BuildContext context, Widget? child) {
        return Transform.translate(
          offset: Offset(widget.deltaX * shake(offsetAnimation.value), 0),
          child: child,
        );
      },
      child: widget.child,
    );
  }
}

Benavidez answered 20/7, 2021 at 3:11 Comment(1)
Feels nicer with 1500 ms duration. Great custom widget thanks!Sidran
V
2

Here is some code from my app. It shakes a red x on the screen. redx.png. I'm sure you could adopt it to your use case. I'm using AnimatedBuilder.

Code in action: https://giphy.com/gifs/Yo2u06oMu1ksPYRD3B


import 'package:flutter/material.dart';
class ShakeX extends StatefulWidget {
  const ShakeX({
    Key key,
  }) : super(key: key);

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

class _ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin{
  AnimationController controller;


  @override
  void initState() {
    controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
        final Animation<double> offsetAnimation =
    Tween(begin: 0.0, end: 24.0).chain(CurveTween(curve: Curves.elasticIn)).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        }
      });
    controller.forward(from: 0.0);
    return AnimatedBuilder(animation: offsetAnimation, 
    builder: (context, child){
                  if (offsetAnimation.value < 0.0) print('${offsetAnimation.value + 8.0}');
                  return Container(
                    margin: EdgeInsets.symmetric(horizontal: 24.0),
                    padding: EdgeInsets.only(left: offsetAnimation.value + 30.0, right: 30.0 - offsetAnimation.value),
               child: Image.asset("assets/redx.png"),
                  );
    },);
  }
}
Veron answered 1/12, 2019 at 10:15 Comment(0)
F
0

For those still looking... You can achieve something like that using Animated Widget

ShakeAnimatedWidget(
      enabled: this._enabled,
      duration: Duration(milliseconds: 1500),
      shakeAngle: Rotation.deg(z: 40),
      curve: Curves.linear,
      child: FlutterLogo(
        style: FlutterLogoStyle.stacked,
      ),
    ),

Check the link for even advance usage. Shaking flutter logo

Frankfurter answered 6/12, 2021 at 16:34 Comment(0)
R
0

There are many animation based packages in flutter, you can check that site (https://fluttergems.dev/animation-transition/) to see them. As developer, you don't have to to create animation classes from scratch.

Regarding shaking animation, I would suggest flutter_animator package. There is a shake widget which exactly performs what you need.

Resor answered 18/2, 2022 at 13:56 Comment(0)
H
0

My version on previous code. It prevents usage of paddings and margins

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

class ShakeX extends StatefulWidget {
  final Widget child;
  final double horizontalPadding;
  final double animationRange;
  final ShakeXController shakeXController;
  final Duration animationDuration;

  const ShakeX({
    Key? key,
    required this.child,
    this.horizontalPadding = 16,
    this.animationRange = 16,
    required this.shakeXController,
    this.animationDuration = const Duration(milliseconds: 500),
  }) : super(key: key);

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

class ShakeXState extends State<ShakeX> with SingleTickerProviderStateMixin {
  late AnimationController animationController;
  var childSize = Size.zero;

  @override
  void initState() {
    super.initState();

    animationController = AnimationController(duration: widget.animationDuration, vsync: this);
    widget.shakeXController.setState(this);
  }

  @override
  Widget build(BuildContext context) {
    final Animation<double> offsetAnimation = Tween(begin: 0.0, end: widget.animationRange)
        .chain(CurveTween(curve: Curves.elasticIn))
        .animate(animationController)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed && status != AnimationStatus.reverse) {
          animationController.reverse();
        }
      });

    return AnimatedBuilder(
      animation: offsetAnimation,
      builder: (context, child) {
        return SizedBox(
          width: childSize.width,
          height: childSize.height,
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              Positioned(
                left: offsetAnimation.value,
                child: MeasureSize(
                    onChange: (size) {
                      setState(() {
                        childSize = size;
                      });
                    },
                    child: widget.child),
              ),
            ],
          ),
        );
      },
    );
  }
}

class ShakeXController {
  late ShakeXState _state;
  void setState(ShakeXState state) {
    _state = state;
  }

  Future<void> shake() {
    return _state.animationController.forward(from: 0.0);
  }
}

typedef void OnWidgetSizeChange(Size size);

class MeasureSizeRenderObject extends RenderProxyBox {
  Size? oldSize;
  OnWidgetSizeChange onChange;

  MeasureSizeRenderObject(this.onChange);

  @override
  void performLayout() {
    super.performLayout();

    Size newSize = child!.size;
    if (oldSize == newSize) return;

    oldSize = newSize;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      onChange(newSize);
    });
  }
}

class MeasureSize extends SingleChildRenderObjectWidget {
  final OnWidgetSizeChange onChange;

  const MeasureSize({
    Key? key,
    required this.onChange,
    required Widget child,
  }) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return MeasureSizeRenderObject(onChange);
  }

  @override
  void updateRenderObject(BuildContext context, covariant MeasureSizeRenderObject renderObject) {
    renderObject.onChange = onChange;
  }
}
Hircine answered 11/1, 2023 at 12:29 Comment(0)
M
0

In my case, I needed the ability to control the shake animation outside of the ShakeWidget. I didn't want to add GlobalKey to get access to the state of the widget, I don't like to use late methods if possible and changing keys each time I want to trigger shaking animation also looks hacky to me. So I created a widget that gets a controller to control the shaking animation:

import 'dart:math';

import 'package:flutter/material.dart';

/// A controller that triggers a shake animation
/// Call [shake] to trigger the shake
class ShakeController extends ChangeNotifier {
  void shake() => notifyListeners();
}

/// A widget that shakes its child when the controller triggers
/// - [duration] is the duration of the shake
/// - [deltaX] is the maximum distance to shake
/// - [oscillations] is the number of oscillations of the shake
/// - [curve] is the curve of the shake
/// - [controller] is the controller that triggers the shake
/// - [child] is the child to shake
class ShakeWidget extends StatefulWidget {
  const ShakeWidget({
    super.key,
    required this.child,
    required this.controller,
    this.duration = const Duration(milliseconds: 500),
    this.deltaX = 4,
    this.oscillations = 6,
    this.curve = Curves.linear,
  });

  final Duration duration;
  final double deltaX;
  final int oscillations;
  final Widget child;
  final Curve curve;
  final ShakeController controller;

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

class _ShakeWidgetState extends State<ShakeWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: widget.duration,
    );

    _animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: widget.curve),
    );

    widget.controller.addListener(_startShaking);
  }

  @override
  void dispose() {
    widget.controller.removeListener(_startShaking);
    _animationController.dispose();
    super.dispose();
  }

  void _startShaking() {
    _animationController.forward(from: 0);
  }

  /// Create a sinusoidal curve that starts and ends at 0
  /// Oscillates with increasing amplitude to the middle and then decreasing
  /// amplitude to the end.
  double _wave(double t) =>
      sin(widget.oscillations * 2 * pi * t) * (1 - (2 * t - 1).abs());

  @override
  Widget build(BuildContext context) => AnimatedBuilder(
        animation: _animation,
        builder: (context, child) => Transform.translate(
          offset: Offset(
            widget.deltaX * _wave(_animation.value),
            0,
          ),
          child: widget.child,
        ),
        child: widget.child,
      );
}

With the linear Curve the movement will be looking like this image:

Sinusoidal Shaking Curve

To start the shaking animation, you just need to call the shake() method on the controller:

class ShakeExample extends StatelessWidget {
  final ShakeController _shakeController = ShakeController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shake Widget Example'),
      ),
      body: Center(
        child: ShakeWidget(
          controller: _shakeController,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
            child: Center(child: Text('Shake Me')),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _shakeController.shake();
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

You can play with the results in this DartPad:

https://dartpad.dev/?id=1eb04fbedb742261031af79afdc9fd46

Mousebird answered 16/5 at 11:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.