我真的很喜欢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,
);
},
);
}
下面是这个例子的完整源代码。
就这样了。现在去摇动你的小部件吧!😎
编码愉快!