动画能提高用户的使用体验,使APP更流畅。那么在Flutter中如何实现动画以及选择使用什么样的动画呢?
开门见山,我们直接上图:
绘制依赖的动画
绘制依赖的动画是指我们用动画库没法直接实现的动画,这时候有两种实现方式:
- 用Canvas不断绘制形成动画(CustomPainter或者自定义RenderObjectWidget);
- 使用设计师提供的动画文件,然后结合三方库来使用(Lottie或者Flare等)。
Lottie
lottie-flutter借鉴自Lottie,使用方法很也很简单。
- 添加依赖
dependencies:
lottie: ^1.0.1
- 引入库文件
import 'package:lottie/lottie.dart';
- 使用
Lottie.network('https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json')
CustomPainter
CustomPainter是系统提供的一个能够绘制内容的底层API。
- CustomPainter绘制
class SquarePainter extends CustomPainter {
final double radians;
SquarePainter(this.radians);
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.pink
..strokeWidth = 5
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
var path = Path();
var angle = (math.pi * 2) / 4.0;
Offset center = Offset(size.width / 2, size.height / 2);
Offset startPoint =
Offset(100 * math.cos(radians), 100 * math.sin(radians));
path.moveTo(startPoint.dx + center.dx, startPoint.dy + center.dy);
for (int i = 1; i <= 4; i++) {
double x = 100 * math.cos(radians + angle * i) + center.dx;
double y = 100 * math.sin(radians + angle * i) + center.dy;
path.lineTo(x, y);
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
- CustomPaint这个Widget封装CustomPainter的绘制内容
CustomPaint(
painter: SquarePainter(animation.value),
child: Container(),
);
- 用AnimationController驱动动画
提示: CustomPainter可以实现很复杂的绘制,本案例仅仅用CustomPainter简单绘制了一个正方形,然后进行不断的旋转动画。
隐性动画
隐性动画的Widget是ImplicitlyAnimatedWidget,它的特点是在属性发生改变后会自动进行动画,不需对动画进行控制,所以叫做隐性动画。
Flutter中的隐性动画和iOS中的隐性动画的概念类似。
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
const ImplicitlyAnimatedWidget({
Key? key,
this.curve = Curves.linear,
required this.duration,
this.onEnd,
})
}
ImplicitlyAnimatedWidget可以设置动画的速率曲线curve
(可以设置的类型),动画时间duration
和动画结束后的回调函数onEnd
。属性发生变化后,动画就依据这些参数自动进行。
官方提供了很多的隐性动画Widget,他们被命名为Animated**
。接下来我们就来一个个看下这些Widget的动画效果。
AnimatedAlign
Align的可动画版本,alignment的发生变化引发动画效果。
class Body extends StatefulWidget {
@override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> with TickerProviderStateMixin {
bool selected = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CustomPainter'),
),
body: GestureDetector(
onTap: () {
setState(() {
selected = !selected;
});
},
child: Center(
child: Container(
width: 250.0,
height: 250.0,
color: Colors.red,
child: AnimatedAlign(
alignment: selected ? Alignment.topRight : Alignment.bottomLeft,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Logo(),
),
),
),
),
);
}
}
AnimatedContainer
Container的可动画版本,AnimatedContainer的各种属性发生变化后有动画效果。
AnimatedContainer(
width: selected ? 300.0 : 150.0,
height: selected ? 300.0 : 150.0,
decoration: BoxDecoration(
color: selected ? Colors.amber[500] : Colors.amber[200],
borderRadius: BorderRadius.circular(selected ? 20 : 0)),
alignment: selected
? AlignmentDirectional.bottomCenter
: AlignmentDirectional.topCenter,
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Logo(),
)
AnimatedDefaultTextStyle
TextStyle的可动画版本,TextStyle发生变化引发动画效果。
AnimatedDefaultTextStyle(
duration: Duration(seconds: 1),
curve: Curves.bounceOut,
style: TextStyle(
fontSize: selected ? 20 : 16,
color: selected ? Colors.amber : Colors.red,
fontWeight: FontWeight.bold,
),
child: Text("Animated DefaultTextStyle"),
)
AnimatedOpacity
Opacity的可动画版本,透明度改变引发动画效果。
AnimatedOpacity(
opacity: selected ? 1.0 : 0.0,
duration: Duration(seconds: 1),
curve: Curves.linear,
child: Logo(),
)
AnimatedPadding
Padding的可动画版本,Padding改变引发动画效果。
AnimatedPadding(
curve: Curves.easeInOut,
duration: Duration(seconds: 1),
child: Container(
child: Logo(),
),
padding: EdgeInsets.symmetric(
horizontal: selected ? 10 : 40,
vertical: selected ? 10 : 30),
)
AnimatedPhysicalModel
PhysicalModel的可动画版本,borderRadius和elevation改变引发动画效果。
AnimatedPhysicalModel(
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn,
elevation: selected ? 0 : 10.0,
shape: BoxShape.rectangle,
shadowColor: Colors.red,
color: Colors.white,
borderRadius: selected
? BorderRadius.all(Radius.circular(0))
: BorderRadius.all(Radius.circular(10)),
child: Container(
color: Colors.blue[100],
child: Logo(),
),
)
AnimatedPositioned
Position的可动画版本,Position改变引发动画效果。
Stack(
children: [
AnimatedPositioned(
width: selected ? 100.0 : 100.0,
height: selected ? 100.0 : 100.0,
top: selected ? 150.0 : 50.0,
left: selected ? 150.0 : 100.0,
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Container(
color: Colors.blue,
),
),
],
),
AnimatedCrossFade
两个子Widget相互切换的动画。
AnimatedCrossFade(
firstChild: Logo(),
secondChild: FlutterLogo(
size: 120,
),
crossFadeState: selected
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: Duration(seconds: 1),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeIn,
)
AnimatedSize
当子Widget大小发生变化后会发生动画。AnimatedContainer是自身大小发生变化引发动画,这是它们的主要区别。
解释:子Widget(
Colors.amber
)的大小改变没有动画,子Widget的大小改变后AnimatedSize(Colors.red
)开始执行动画。
AnimatedSize(
duration: Duration(seconds: 1),
reverseDuration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Container(
width: selected ? 150 : 100,
height: selected ? 150 : 100,
color: Colors.amber,
),
vsync: this,
)
AnimatedSwitcher
这个Widget功能比较全,可以实现
- 添加、删除Widget的动画;
- 切换Widget的动画;
- Widget属性变化的动画;
AnimatedSwitcher(
child: Text(
"$elapsed",
key: ValueKey(elapsed),
style: TextStyle(fontSize: 34),
),
duration: Duration(seconds: 2),
transitionBuilder:
(Widget child, Animation<double> animation) {
final offsetAnimation = TweenSequence([
TweenSequenceItem(
tween: Tween<Offset>(
begin: Offset(0.0, 1.0),
end: Offset(0.0, 0.0))
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 1),
TweenSequenceItem(
tween: ConstantTween(Offset(0.0, 0.0)), weight: 4),
]).animate(animation);
return ClipRRect(
child: SlideTransition(
position: offsetAnimation,
child: child,
),
);
},
layoutBuilder: (currentChild, previousChildren) {
return currentChild;
},
)
前面介绍了一系列官方提供的隐性动画Widget,除了AnimatedCrossFade
,AnimatedSize
和AnimatedSwitcher
外都是ImplicitlyAnimatedWidget的子类。使用这些类能够方便的实现动画,当遇到没法实现的功能又想用隐性动画的需求时,TweenAnimationBuilder就是我们很好的选择。
TweenAnimationBuilder
实现自定义隐性动画。
TweenAnimationBuilder(
duration: Duration(seconds: 2),
curve: Curves.easeInOut,
tween: Tween<double>(begin: 0, end: selected ? 180 : 0),
builder: (context, value, child) {
return RotationWidget(rotationY: value);
},
)
class RotationWidget extends StatelessWidget {
static const double degrees2Radians = pi / 180;
final double rotationY;
const RotationWidget({Key key, this.rotationY = 0}) : super(key: key);
@override
Widget build(BuildContext context) {
return Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(rotationY * degrees2Radians),
child: rotationY <= 90
? Logo(size: 250)
: Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(180 * degrees2Radians),
child: FlutterLogo(size: 200),
));
}
}
显性动画
显性动画是指需要开发者去控制动画,也就是说需要开发者去使用动画Animation相关的类去控制动画过程。
动画(Animation
)相关类
- Animation
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
void addStatusListener(AnimationStatusListener listener);
void removeStatusListener(AnimationStatusListener listener);
AnimationStatus get status;
T get value;
bool get isDismissed => status == AnimationStatus.dismissed;
bool get isCompleted => status == AnimationStatus.completed;
}
Animation这个类代表了动画的当前值和状态,以及告知监听者这两个值的改变:
-
value
代表动画当前的值,addListener()
和removeListener()
可以添加和移除监听者,当value
变化后,会通知监听者值的变化; -
status
代表动画的状态,有初始状态dismissed
,正向动画状态forward
,反向动画状态reverse
和动画已完成状态completed
四种,addStatusListener()
和removeStatusListener()
可以添加和移除监听者,当status
变化后,会通知监听者状态的变化。
- AnimationController
class AnimationController extends Animation<double> {
void reset() {}
TickerFuture forward({ double? from }) {}
TickerFuture reverse({ double? from }) {}
TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {}
TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {}
TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {}
void stop({ bool canceled = true }) {}
}
Animation只能代表当前动画值和动画的状态,没法控制动画。AnimationController继承自Animation,是对动画进行控制的类。譬如它能重置动画reset()
,正向进行动画forward()
,反向进行动画reverse()
,重复进行动画repeat()
和停止动画stop()
等。
- CurvedAnimation
class CurvedAnimation extends Animation<double> {
final Animation<double> parent;
Curve curve;
Curve? reverseCurve;
}
CurvedAnimation的功能是给parent
设置一个动画速率变化的曲线。也就是说动画值的变化率可以不是固定的。curve
是正向动画的曲线,reverseCurve
是反向动画的曲线。可以设置的类型和隐性动画一样
- Tween
class Tween<T extends dynamic> extends Animatable<T> {
T? begin;
T? end;
Tween主要是给``设置一个动画的开始值begin
和动画的结束值end
。
系统提供了一些列的Tween封装类供我们使用:
ColorTween
SizeTween
RectTween
IntTween
StepTween
ConstantTween
CurveTween
- TweenSequence
class TweenSequence<T> extends Animatable<T> {
final List<TweenSequenceItem<T>> _items = <TweenSequenceItem<T>>[];
final List<_Interval> _intervals = <_Interval>[];
}
TweenSequence可以设置一系列的TweenSequenceItem,相当于设置一些关键帧,可以实现类似关键帧动画。
动画实现的案例
我们来用上面提到的一些类来实现一个图片放大然后缩小的循环动画,效果如下:
- 1.创建AnimationController
// vsync参数值是同步信号,duration参数值是动画的时间
AnimationController _controller = AnimationController(vsync: this, duration: Duration(seconds: 2));
- 2.给
_controller
设置Curve
// parent参数值是需要设置Curve的Animation,curve参数值是正向动画的Curve,reverseCurve参数值是反向动画的Curve
Animation _animation = CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut,
reverseCurve: Curves.bounceIn);
- 3.给
_animation
设置动画的开始值和结束值
Animation_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
- 4.监听
_controller
动画值的改变,然后进行界面更新
_controller.addListener(() {
setState(() {});
});
- 5.监听听
_controller
动画的状态改变,然后进行循环动画
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
- 6.界面的展示逻辑
Logo(
size: _sizeAnim.value,
)
通过上面几个步骤,动画的代码就写完了。所有代码如下:
class Body extends StatefulWidget {
@override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
// 创建AnimationController
AnimationController _controller;
Animation _animation;
Animation _sizeAnim;
@override
void initState() {
super.initState();
// 1.创建AnimationController
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 2));
// 2.设置Curve的值
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut,
reverseCurve: Curves.bounceIn);
// 3. 设置动画的开始值和结束值
_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
// 4. 监听动画值的改变
_controller.addListener(() {
setState(() {});
});
// 5. 监听动画的状态改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animation"),
),
body: Center(
child: Logo(
size: _sizeAnim.value,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
_controller.forward();
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
AnimatedWidget
上面的代码我们实现了动画,但是有两个问题:
- 我们需要监听
_controller
的值的变化,然后调用setState(() {});
这个方法; - 动画的绘制涉及到了Body的重构ReBuild,其实我们只需要Logo进行重构ReBuild就行。
我们可以将Logo重构为一个AnimatedWidget,这样就可以解决上面两问题了:
class LogoAnimatedWidget extends AnimatedWidget {
LogoAnimatedWidget(Animation anim) : super(listenable: anim);
@override
Widget build(BuildContext context) {
Animation anim = listenable;
return Logo(size: anim.value);
}
}
解释下代码逻辑:
- 继承AnimatedWidget的子类的构造函数需要传入一个Animation;
- 重写
build
方法,返回Widget,这里就是我们的Logo。
接下来删除_controller.addListener()
, 然后使用LogoAnimatedWidget。
class Body extends StatefulWidget {
@override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
// 创建AnimationController
AnimationController _controller;
Animation _animation;
Animation _sizeAnim;
@override
void initState() {
super.initState();
// 1.创建AnimationController
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 2));
// 2.设置Curve的值
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut,
reverseCurve: Curves.bounceIn);
// 3. 设置动画的开始值和结束值
_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
// 4. 监听动画值的改变
// _controller.addListener(() {
// setState(() {});
// });
// 5. 监听动画的状态改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animation"),
),
body: Center(
child: LogoAnimatedWidget(_sizeAnim),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
_controller.forward();
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class LogoAnimatedWidget extends AnimatedWidget {
LogoAnimatedWidget(Animation anim) : super(listenable: anim);
@override
Widget build(BuildContext context) {
Animation anim = listenable;
return Logo(size: anim.value);
}
}
AnimatedBuilder
AnimatedWidget其实也有一些问题:
- 需要新建一个AnimatedWidget,代码量增加了;
- AnimatedWidget如果动画的Widget含有子Widget,那子Widget也会重构ReBuild。
AnimatedBuilder可以解决上面两问题。代码如下:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animation"),
),
body: Center(
child: AnimatedBuilder(
animation: _sizeAnim,
builder: (ctx, child) {
return Logo(
size: _sizeAnim.value,
);
}),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
_controller.forward();
},
),
);
}
AnimatedBuilder有两个参数,animation
参数值是Animation, builder
参数值是一个函数,会提供BuildContext和子Widget,一个提供Widget的构造环境,一个提供child可以复用,无需重构子Widget。
官方提供的AnimatedWidget
官方提供了一些AnimatedWidget,使用方式和使用自定义的AnimatedWidget类似。
由于和隐式动画的版本类似,这里就不一一贴出效果了,只是列出来供大家参阅:
AlignTransition
DecoratedBoxTransition
DefaultTextStyleTransition
PositionedTransition
RelativePositionedTransition
RotationTransition
ScaleTransition
SizeTransition
SlideTransition
FadeTransition
总结
至此,我们将Flutter中的动画实现方式总结完了。
我们知道动画的逻辑就是不断的重绘,那Animation相关的类是如何引发重绘的呢?隐式动画又是如何对开发者屏蔽了Animation类实现动画的呢?
相关的问题,我们在下一节将深入底层去为你揭开面纱!