1. Flutter动画简介
Flutter中对动画进行了抽象,主要涉及 Animation、Curve、Controller、Tween这四个角色。
Animation
Animation是一个抽象类,它本身和UI渲染没有任何关系,而它主要的功能是保存动画的插值和状态
动画通知
我们可以通过Animation来监听动画每一帧以及执行状态的变化,Animation有如下两个方法:
addListener();它可以用于给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建。addStatusListener();它可以给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向(见AnimationStatus定义)时会调用状态改变的监听器。
Curve
动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。
可以通过CurvedAnimation来指定动画的曲线
final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves类是一个预置的枚举类,定义了许多常用的曲线,下面列几种常用的:
| Curves曲线 | 动画过程 |
|---|---|
| linear | 匀速的 |
| decelerate | 匀减速 |
| ease | 开始加速,后面减速 |
| easeIn | 开始慢,后面快 |
| easeOut | 开始快,后面慢 |
| easeInOut | 开始慢,然后加速,最后再减速 |
当然我们也可以创建自己Curve,例如我们定义一个正弦曲线:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
这个就涉及到我的弱点了,需要计算曲线。什么正弦余弦。
AnimationController
纯理论知识,看着头大。
AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。
AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用。 但是,AnimationController具有控制动画的其他方法,例如forward()方法可以启动正向动画,reverse()可以启动反向动画。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
duration表示动画执行的时长,通过它我们可以控制动画的速度。
Ticker
当创建一个AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:
abstract class TickerProvider {
//通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter 应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。(这里只看懂了表面意思)
通常我们会将SingleTickerProviderStateMixin添加到State的定义中,然后将State对象作为vsync的值。
Tween
默认情况下,AnimationController对象值的范围是[0.0,1.0]。如果我们需要构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween来添加映射以生成不同的范围或数据类型的值。例如,像下面示例,Tween生成[-200.0,0.0]的值:
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
Tween构造函数需要begin和end两个参数。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为[0.0,1.0],但这不是必须的,我们可以自定义需要的范围。
Tween继承自Animatable<T>,而不是继承自Animation<T>,Animatable中主要定义动画值的映射规则。
Tween.animate
要使用 Tween 对象,需要调用其animate()方法,然后传入一个控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
注意animate()返回的是一个Animation,而不是一个Animatable。
以下构建了一个控制器、一条曲线和一个 Tween:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
2. 动画基本结构及状态监听
2.1 动画基本结构
在Flutter中我们可以通过多种方式来实现动画,下面通过一个图片逐渐放大示例的不同实现来演示Flutter中动画的不同实现方式的区别。
基础版本
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
//匀速
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() => {});
});
//启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
上面代码中addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,改变Image的宽高,因为它的高度和宽度现在使用的是animation.value ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。
上面的例子中并没有指定Curve,所以放大的过程是线性的(匀速),下面我们指定一个Curve,来实现一个类似于弹簧效果的动画过程,我们只需要将initState中的代码改为下面这样即可:
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//使用弹性曲线
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() => {});
});
//启动动画
controller.forward();
}
使用AnimatedWidget简化
上面示例中通过addListener()和setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,并允许我们将 widget 分离出来,重构后的代码如下:(个人感觉没什么用,毕竟为了这个简化还好吧Image封装成一个类。感觉代码不太优雅)
import 'package:flutter/material.dart';
class AnimatedImage extends AnimatedWidget {
const AnimatedImage({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
const ScaleAnimationRoute1({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2), vsync: this);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(
animation: animation,
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
用AnimatedBuilder重构
用AnimatedWidget 可以从动画中分离出 widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget 中,假设如果我们再添加一个 widget 透明度变化的动画,那么我们需要再实现一个AnimatedWidget(这就是我上面说的为了给Image添加动画把他抽出来不太优雅),这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来, 上面的 build 方法中的代码可以改为:
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("imgs/avatar.png"),
builder: (BuildContext ctx, child) {
return Center(
child: SizedBox(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
也许你会说这和我们刚开始的示例差不了多少,其实它会带来三个好处:
- 不用显式的去添加帧监听器,然后再调用
setState()了,这个好处和AnimatedWidget是一样的。 - 更好的性能:因为动画每一帧需要构建的 widget 的范围缩小了,如果没有
builder,setState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。 - 通过
AnimatedBuilder可以封装常见的过渡效果来复用动画。下面我们通过封装一个GrowTransition来说明,它可以对子widget实现放大动画:
封装一个动画:
class GrowTransition extends StatelessWidget {
const GrowTransition({Key? key,
required this.animation,
this.child,
}) : super(key: key);
final Widget? child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
然后使用
Widget build(BuildContext context) {
return GrowTransition(
child: Image.asset("images/avatar.png"),
animation: animation,
);
}
注:这里是吧动画封装起来了,这才是开发中经常用到的,也符合编程的原则 “抽象不应该依赖于实现,实现应该依赖于抽象”.
Flutter中正是通过这种方式封装了很多动画,如:FadeTransition、ScaleTransition、SizeTransition等,很多时候都可以复用这些预置的过渡类。
2.2 动画状态监听
我们可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:
3. 自定义路由切换动画
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。现在,我们如果在Android上也想使用左右切换风格,该怎么做?一个简单的作法是可以直接使用
Navigator.push(context, CupertinoPageRoute(
builder: (context)=>SSLAnimationTest(),
));
然后也可以自己通过PageRouteBuilder来自定义路由切换动画。例如我们想以渐隐渐入动画来实现路由过渡,实现代码如下:
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500), //动画时间为500毫秒
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return FadeTransition(
//使用渐隐渐入过渡,
opacity: animation as Animation<double>,//这里需要转一下类型
child: SSLAnimationTest(), //路由B
);
},
),
);
无论是MaterialPageRoute、CupertinoPageRoute,还是PageRouteBuilder,它们都继承自PageRoute类,而PageRouteBuilder其实只是PageRoute的一个包装,我们可以直接继承PageRoute类来实现自定义路由,上面的例子可以通过如下方式实现:
1.定义一个路由类FadeRoute
class FadeRoute extends PageRoute {
FadeRoute({
required this.builder,
this.transitionDuration = const Duration(milliseconds: 300),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor = Colors.red,
this.barrierLabel = '',
this.maintainState = true,
});
final WidgetBuilder builder;
@override
final Duration transitionDuration;
@override
final bool opaque;
@override
final bool barrierDismissible;
@override
final Color barrierColor;
@override
final String barrierLabel;
@override
final bool maintainState;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) => builder(context);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}
}
2.使用
Navigator.push(context, FadeRoute(builder: (context) {
return SSLAnimationTest();
}));
假如我们只想在打开新路由时应用动画,而在返回时不使用动画,那么我们在构建过渡动画时就必须判断当前路由isActive属性是否为true,代码如下:
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
//当前路由被激活,是打开新路由
if(isActive) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}else{
//是返回,则不应用过渡动画
return Padding(padding: EdgeInsets.zero);
}
}
4.Hero动画
假设有两个路由A和B,他们的内容交互如下:
A:包含一个用户头像,圆形,点击后跳到B路由,可以查看大图。
B:显示用户头像原图,矩形。
在AB两个路由之间跳转的时候,用户头像会逐渐过渡到目标路由页的头像上,接下来我们先看看代码,然后再解析。
class HeroAnimationRouteA extends StatelessWidget {
const HeroAnimationRouteA({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: Column(
children: <Widget>[
InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.asset(
"imgs/avatar.png",
width: 50.0,
),
),
),
onTap: () {
//打开B路由
Navigator.push(context, PageRouteBuilder(
pageBuilder: (
BuildContext context,
animation,
secondaryAnimation,
) {
return FadeTransition(
opacity: animation,
child: Scaffold(
appBar: AppBar(
title: const Text("原图"),
),
body: const HeroAnimationRouteB(),
),
);
},
));
},
),
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text("点击头像"),
)
],
),
);
}
}
路由B
lass HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("imgs/avatar.png"),
),
);
}
}
实现 Hero 动画只需要用Hero组件将要共享的 widget 包装起来,并提供一个相同的 tag 即可,中间的过渡帧都是 Flutter 框架自动完成的。必须要注意, 前后路由页的共享Hero的 tag 必须是相同的,Flutter 框架内部正是通过 tag 来确定新旧路由页widget的对应关系的。
5.交织动画
有些时候我们可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成,比如:有一个柱状图,需要在高度增长的同时改变颜色,等到增长到最大高度后,我们需要在X轴上平移一段距离。可以发现上述场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画(Stagger Animation)会非常简单。交织动画需要注意以下几点:
- 要创建交织动画,需要使用多个动画对象(
Animation)。 - 一个
AnimationController控制所有的动画对象。 - 给每一个动画对象指定时间间隔(Interval)
下面我们看一个例子,实现一个柱状图增长的动画:
- 开始时高度从0增长到300像素,同时颜色由绿色渐变为红色;这个过程占据整个动画时间的60%。
- 高度增长到300后,开始沿X轴向右平移100像素;这个过程占用整个动画时间的40%。
将执行动画的Widget分离出来:
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({
Key? key,
required this.controller,
}) : super(key: key) {
//高度动画
height = Tween<double>(
begin: .0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
color = ColorTween(
begin: Colors.green,
end: Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
padding = Tween<EdgeInsets>(
begin: const EdgeInsets.only(left: .0),
end: const EdgeInsets.only(left: 100.0),
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.6, 1.0, //间隔,后40%的动画时间
curve: Curves.ease,
),
),
);
}
late final Animation<double> controller;
late final Animation<double> height;
late final Animation<EdgeInsets> padding;
late final Animation<Color?> color;
Widget _buildAnimation(BuildContext context, child) {
return Container(
alignment: Alignment.bottomCenter,
padding: padding.value,
child: Container(
color: color.value,
width: 50.0,
height: height.value,
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: controller,
);
}
}
StaggerAnimation中定义了三个动画,分别是对Container的height、color、padding属性设置的动画,然后通过Interval来为每个动画指定在整个动画过程中的起始点和终点。下面我们来实现启动动画的路由:
class StaggerRoute extends StatefulWidget {
@override
_StaggerRouteState createState() => _StaggerRouteState();
}
class _StaggerRouteState extends State<StaggerRoute>
with TickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
}
_playAnimation() async {
try {
//先正向执行动画
await _controller.forward().orCancel;
//再反向执行动画
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because we were disposed
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
ElevatedButton(
onPressed: () => _playAnimation(),
child: Text("start animation"),
),
Container(
width: 300.0,
height: 300.0,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
border: Border.all(
color: Colors.black.withOpacity(0.5),
),
),
//调用我们定义的交错动画Widget
child: StaggerAnimation(controller: _controller),
),
],
),
);
}
}
执行效果:
6.动画切换组件(AnimatedSwitcher)
实际开发中,我们经常会遇到切换UI元素的场景,比如Tab切换、路由切换。为了增强用户体验,通常在切换时都会指定一个动画,以使切换过程显得平滑。Flutter SDK组件库中已经提供了一些常用的切换组件,如PageView、TabView等,但是,这些组件并不能覆盖全部的需求场景,为此,Flutter SDK中提供了一个AnimatedSwitcher组件,它定义了一种通用的UI切换抽象。
1. AnimatedSwitcher
AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画。也就是说在AnimatedSwitcher的子元素发生变化时,会对其旧元素和新元素做动画,我们先看看AnimatedSwitcher 的定义:
const AnimatedSwitcher({
Key? key,
this.child,
required this.duration, // 新child显示动画时长
this.reverseDuration,// 旧child隐藏的动画时长
this.switchInCurve = Curves.linear, // 新child显示的动画曲线
this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
当AnimatedSwitcher的 child 发生变化时(类型或 Key 不同),旧 child 会执行隐藏动画,新 child 会执行执行显示动画。究竟执行何种动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的 builder,
typedef AnimatedSwitcherTransitionBuilder =
Widget Function(Widget child, Animation<double> animation);
该builder在AnimatedSwitcher的child切换时会分别对新、旧child绑定动画:
- 对旧child,绑定的动画会反向执行(reverse)
- 对新child,绑定的动画会正向指向(forward)
这样一下,便实现了对新、旧child的动画绑定。AnimatedSwitcher的默认值是AnimatedSwitcher.defaultTransitionBuilder :
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
可以看到,返回了FadeTransition对象,也就是说默认情况,AnimatedSwitcher会对新旧child执行“渐隐”和“渐显”动画。
下面我们看一个列子:实现一个计数器,然后在每一次自增的过程中,旧数字执行缩小动画隐藏,新数字执行放大动画显示,代码如下:
import 'package:flutter/material.dart';
class AnimatedSwitcherCounterRoute extends StatefulWidget {
const AnimatedSwitcherCounterRoute({Key key}) : super(key: key);
@override
_AnimatedSwitcherCounterRouteState createState() => _AnimatedSwitcherCounterRouteState();
}
class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
//执行缩放动画
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$_count',
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
key: ValueKey<int>(_count),
style: Theme.of(context).textTheme.headline4,
),
),
ElevatedButton(
child: const Text('+1',),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
运行代码,当点击“+1”按钮时,原先的数字会逐渐缩小直至隐藏,而新数字会逐渐放大,如图
注意:AnimatedSwitcher的新旧child,如果类型相同,则Key必须不相等。
2 AnimatedSwitcher高级用法
假设现在我们想实现一个类似路由平移切换的动画:旧页面屏幕中向左侧平移退出,新页面从屏幕右侧平移进入。
前面说过在AnimatedSwitcher的 child 切换时会对新child执行正向动画(forward),而对旧child执行反向动画(reverse),所以真正的效果便是:新 child 确实从屏幕右侧平移进入了,但旧child却会从屏幕右侧(而不是左侧)退出。其实也很容易理解,因为在没有特殊处理的情况下,同一个动画的正向和逆向正好是相反(对称)的。
可以打破这种对称性,那么便可以实现这个功能了.
下面是对动画的封装。
class MySlideTransition extends AnimatedWidget {
const MySlideTransition({
Key? key,
required Animation<Offset> position,
this.transformHitTests = true,
required this.child,
}) : super(key: key, listenable: position);
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<Offset>;
Offset offset = position.value;
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
这样就实现了类似路由切换时的动画。
SlideTransitionX
刚才实现了“左出右入”的动画,那如果要实现“左入右出”、“上入下出”或者 “下入上出”怎么办?当然,我们可以分别修改上面的代码,但是这样每种动画都得单独定义一个“Transition”,这很麻烦。 可以使用SlideTransitionX封装一个通用的动画,然后通过设置不同的枚举来决定使用那种切换方向。
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key? key,
required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
required this.child,
}) : super(key: key, listenable: position) {
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));
break;
}
}
final bool transformHitTests;
final Widget child;
final AxisDirection direction;
late final Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<double>;
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
使用的时候只需给direction传递不同的方向值即可。
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransitionX(
child: child,
direction: AxisDirection.down, //上入下出
position: animation,
);
},
...//省略其余代码
)
7.动画过渡组件
动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。我们知道,为了方便使用者可以自定义动画的曲线、执行时长、方向等,在前面介绍过的动画封装方法中,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。但是,如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。
Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。如果要封装动画,只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可。下面是使用方法。
需要分两步实现:
1.继承ImplicitlyAnimatedWidget类
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
const AnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
Curve curve = Curves.linear,
required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
final BoxDecoration decoration;
final Widget child;
@override
_AnimatedDecoratedBoxState createState() {
return _AnimatedDecoratedBoxState();
}
}
其中curve、duration、reverseDuration三个属性在ImplicitlyAnimatedWidget中已定义。
2.State类继承自AnimatedWidgetBaseState(该类继承自ImplicitlyAnimatedWidgetState类)。
class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
late DecorationTween _decoration;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_decoration = visitor(
_decoration,
widget.decoration,
(value) => DecorationTween(begin: value),
) as DecorationTween;
}
}
-
可以看到我们实现了
build和forEachTween两个方法。在动画执行过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build方法中我们需要构建每一帧的DecoratedBox状态,因此得算出每一帧的decoration状态,这个我们可以通过_decoration.evaluate(animation)来算出,其中animation是ImplicitlyAnimatedWidgetState基类中定义的对象,_decoration是我们自定义的一个DecorationTween类型的对象,那么现在的问题就是它是在什么时候被赋值的呢?要回答这个问题,我们就得搞清楚什么时候需要对_decoration赋值。我们知道_decoration是一个Tween,而Tween的主要职责就是定义动画的起始状态(begin)和终止状态(end)。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有以下两种情况:AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,即_decoration值为DecorationTween(begin: decoration)。AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin: _decoration.animate(animation),end:decoration)。
现在forEachTween的作用就很明显了,它正是用于来更新Tween的初始值的,在上述两种情况下会被调用,而开发者只需重写此方法,并在此方法中更新Tween的起始状态值即可。而一些更新的逻辑被屏蔽在了visitor回调,我们只需要调用它并给它传递正确的参数即可,visitor方法签名如下:
Tween<T> visitor(
Tween<T> tween, //当前的tween,第一次调用为null
T targetValue, // 终止状态
TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
通过继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类可以快速的实现动画过渡组件的封装,这和我们纯手工实现相比,代码简化了很多。