## 动画基本结构
我们通过实现一个图片逐渐放大的示例来演示一下Flutter中动画的基本结构:
```
class ScaleAnimationRoute extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin{
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(()=>{});
});
//启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return new Center(
child: Image.asset("images/avatar.png",
width: animation.value,
height: animation.value
),
);
}
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
```
上面代码中`addListener()`函数调用了`setState()`,所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的`build()`方法再次被调用,而在`build()`中,改变Image的宽高,因为它的高度和宽度现在使用的是`animation.value` ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用`dispose()`方法)以防止内存泄漏。
上面的例子中并没有指定Curve,所以放大的过程是线性的(匀速),下面我们指定一个Curve,来实现一个类似于弹簧效果的动画过程,我们只需要将`initState`中的代码改为下面这样即可:
```
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//使用弹性曲线
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() {
});
});
//启动动画
controller.forward();
}
```
### 使用AnimatedWidget简化
细心的读者可能已经发现上面示例中通过`addListener()`和`setState()` 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用`setState()`的细节,并允许我们将Widget分离出来,重构后的代码如下:
```
class AnimatedImage extends AnimatedWidget {
AnimatedImage({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: Image.asset("images/avatar.png",
width: animation.value,
height: animation.value
),
);
}
}
class ScaleAnimationRoute extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(animation: animation,);
}
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
```
### 用AnimatedBuilder重构
用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中,假设如果我们再添加一个widget透明度变化的动画,那么我们需要再实现一个AnimatedWidget,这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来, 上面的build方法中的代码可以改为:
```
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("images/avatar.png"),
builder: (BuildContext ctx, Widget child) {
return new Center(
child: Container(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
```
上面的代码中有一个迷惑的问题是,`child`看起来像被指定了两次。但实际发生的事情是:将外部引用child传递给AnimatedBuilder后AnimatedBuilder再将其传递给匿名构造器, 然后将该对象用作其子对象。最终的结果是AnimatedBuilder返回的对象插入到Widget树中。
也许你会说这和我们刚开始的示例差不了多少,其实它会带来三个好处:
1. 不用显式的去添加帧监听器,然后再调用`setState()` 了,这个好处和AnimatedWidget是一样的。
2. 动画构建的范围缩小了,如果没有builder,setState()将会在父widget上下文调用,这将会导致父widget的build方法重新调用,而有了builder之后,只会导致动画widget的build重新调用,这在复杂布局下性能会提高。
3. 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。下面我们通过封装一个GrowTransition来说明,它可以对子widget实现放大动画:
```
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) {
return new Center(
child: new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Container(
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、FractionalTranslation等,很多时候都可以复用这些预置的过渡类。**
## 动画状态监听
上面说过,我们可以通过Animation的`addStatusListener()`方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:
枚举值含义`dismissed`动画在起始点停止`forward`动画正在正向执行`reverse`动画正在反向执行`completed`动画在终点停止#### 示例
我们将上面图片放大的示例改为先放大再缩小再放大……这样的循环动画。要实现这种效果,我们只需要监听动画状态的改变即可,即:在动画正向执行结束时反转动画,在动画反向执行结束时再正向执行动画。代码如下:
```
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 1), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//动画执行结束时反向执行动画
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//动画恢复到初始状态时执行动画(正向)
controller.forward();
}
});
//启动动画(正向)
controller.forward();
}
```
- 缘起
- 起步
- 移动开发技术简介
- Flutter简介
- 搭建Flutter开发环境
- 常见配置问题
- Dart语言简介
- 第一个Flutter应用
- 计数器示例
- 路由管理
- 包管理
- 资源管理
- 调试Flutter APP
- Dart线程模型及异常捕获
- 基础Widgets
- Widget简介
- 文本、字体样式
- 按钮
- 图片和Icon
- 单选框和复选框
- 输入框和表单
- 布局类Widgets
- 布局类Widgets简介
- 线性布局Row、Column
- 弹性布局Flex
- 流式布局Wrap、Flow
- 层叠布局Stack、Positioned
- 容器类Widgets
- Padding
- 布局限制类容器ConstrainedBox、SizeBox
- 装饰容器DecoratedBox
- 变换Transform
- Container容器
- Scaffold、TabBar、底部导航
- 可滚动Widgets
- 可滚动Widgets简介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滚动监听及控制ScrollController
- 功能型Widgets
- 导航返回拦截-WillPopScope
- 数据共享-InheritedWidget
- 主题-Theme
- 事件处理与通知
- 原始指针事件处理
- 手势识别
- 全局事件总线
- 通知Notification
- 动画
- Flutter动画简介
- 动画结构
- 自定义路由过渡动画
- Hero动画
- 交错动画
- 自定义Widget
- 自定义Widget方法简介
- 通过组合现有Widget实现
- 实例:TurnBox
- CustomPaint与Canvas
- 实例:圆形渐变进度条(自绘)
- 文件操作与网络请求
- 文件操作
- Http请求-HttpClient
- Http请求-Dio package
- 实例:Http分块下载
- WebSocket
- 使用Socket API
- Json转Model
- 包与插件
- 开发package
- 插件开发:平台通道简介
- 插件开发:实现Android端API
- 插件开发:实现IOS端API
- 系统能力调用
- 国际化
- 让App支持多语言
- 实现Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系统
- Element和BuildContext
- RenderObject与RenderBox
- Flutter从启动到显示