如何在Flutter中实现抖动的文本效果

498 阅读4分钟

我真的很喜欢Flutter动画如何被用来提高可用性。

这里有一个例子,显示了一个Text widget,当发生一些错误时,它就会抖动。

Flutter的动画API使得实现这一点非常容易。这里有一个分步骤的指南。👇

1.创建一个自定义的正弦曲线

这个效果是通过一个AnimationController 和一个基于正弦函数的自定义曲线来完成的。

首先,这里有一个SineCurve ,在一个2 * pi ,重复正弦函数count 次。

// 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);
  }
}

由于SineCurve 是一个Curve 子类,它可以作为一个参数给任何隐含的动画小部件

2.抽取AnimationController的模板代码

我们需要一个AnimationController 来获得我们想要的效果。为了减少模板代码,让我们定义一个State 子类。

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();
  }
}

关于这个技术的更多细节,请看。如何减少AnimationController的模板代码。Flutter Hooks vs 扩展State类

3.创建一个自定义的ShakeWidget

让我们定义一个StatefulWidget 子类,这个子类需要一个子部件以及一些可定制的属性。

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.创建一个自定义的CurvedAnimation

让我们定义一个带有自定义动画的ShakeWidgetState 类。

// 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.用AnimatedBuilder和Transform.translate使用该动画

让我们用一个自定义的AnimatedBuilder ,定义一个build() 方法。

@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,
      );
    },
  );
}

注意我们是如何将_sineAnimation 作为参数传给AnimatedBuilder ,同时用它来计算偏移值的。请看下面的另一种方法

6.添加一个状态监听器,在完成后重置动画

由于我们想多次 "播放 "动画,我们需要在动画完成时重置AnimationController

@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.添加一个shake()方法

我们的ShakeWidgetState 类需要一个shake() 方法,我们可以从外部调用这个方法来启动动画。

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

8.用一个全局键控制ShakeWidget

在父部件中,我们可以声明一个GlobalKey<ShakeWidgetState> ,并在按钮被按下时用它来调用shake()

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),
          ),
        ),
      ],
    );
  }
}

这就是最终的结果(有一个稍微有趣的用户界面)。

最后说明:隐式动画与显式动画

我们创建的SineCurve 类是一个Curve 子类,所以它可以作为一个参数给任何 隐式动画的小部件.

在这个例子中,我们用它来创建一个自定义的CurvedAnimation ,作为参数传递给我们的AnimatedBuilder

但是由于我们使用的是显式动画,所以我们不需要SineCurve ,甚至不需要_sineAnimation ,就可以开始了。事实上,我们可以通过AnimatedBuilder 代码中直接计算正弦值来得到同样的结果。

@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,
      );
    },
  );
}

下面是这个例子的完整源代码。

就这样了。现在去摇动你的小部件吧!😎

编码愉快!