Flutter了解之动画
目录
0. 重要概念
1. 重要的类
2. 用AnimatedWidget简化、用AnimatedBuilder重构、监听状态
3. 自定义路由切换动画
4. Hero动画(又称:共享元素转换)
5. 交织动画(Stagger Animation)
6. 通用“动画切换”组件(AnimatedSwitcher)
7. 动画过渡组件
动画可以改善用户体验。
在任何系统的UI框架中,动画实现的原理都和电影的原理一样,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。
0. 重要概念
将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。
帧率越高则动画就会越流畅。一般情况下,对于人眼来说,动画帧率超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑,而超过32FPS,人眼基本上就感受不到差别了。
由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标,而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的。
Material组件都带有在其设计规范中定义的标准动画效果(可自定义这些效果)。
- 动画类型(两类)
1. 基于tween(补间动画)。
定义了开始点和结束点、时间线以及定义转换时间和速度的曲线,然后从开始点过渡到结束点。
2. 基于物理(物理动画)。
运动被模拟为与真实世界的行为。
如:掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。
- 动画模式
1. 动画列表或网格
涉及在网格或列表中添加或删除元素时应用动画
2. 共享元素转换
Hero动画:用户从页面中选择一个元素(通常是一个图像),然后打开所选元素的详情页面,在打开详情页时使用动画。
3. 交错动画
动画被分解为较小的动作,其中一些动作被延迟。较小的动画可以是连续的,或者可以部分或完全重叠。
例
AnimatedList显示与ListModel保持同步的卡片列表。当新的item被添加到ListModel或从ListModel中删除时,相应的卡片在UI上也会被添加或删除,并伴有动画效果。
点击一个item选择它,再次点击它会取消选择。点击’+’插入选定的item,点击’ - ‘删除选定的item。 tap处理器会从ListModel<E>中添加或删除items,ListModel<E>是List<E>的简单封装 ,用于保持和AnimatedList的同步。 列表模型为其动画列表提供了一个GlobalKey。它使用该键来调用由AnimatedListState定义的insertItem和removeItem方法。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AnimatedListSample extends StatefulWidget {
@override
_AnimatedListSampleState createState() => new _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
final GlobalKey<AnimatedListState> _listKey = new GlobalKey<AnimatedListState>();
ListModel<int> _list;
int _selectedItem;
int _nextItem;
@override
void initState() {
super.initState();
_list = new ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
// Used to build list items that haven't been removed.
Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
return new CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
// Used to build an item after it has been removed from the list. This method is
// needed because a removed item remains visible until its animation has
// completed (even though it's gone as far this ListModel is concerned).
// The widget will be used by the [AnimatedListState.removeItem] method's
// [AnimatedListRemovedItemBuilder] parameter.
Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
return new CardItem(
animation: animation,
item: item,
selected: false,
// No gesture detector here: we don't want removed items to be interactive.
);
}
// Insert the "next item" into the list model.
void _insert() {
final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
_list.insert(index, _nextItem++);
}
// Remove the selected item from the list model.
void _remove() {
if (_selectedItem != null) {
_list.removeAt(_list.indexOf(_selectedItem));
setState(() {
_selectedItem = null;
});
}
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.add_circle),
onPressed: _insert,
tooltip: 'insert a new item',
),
new IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: _remove,
tooltip: 'remove the selected item',
),
],
),
body: new Padding(
padding: const EdgeInsets.all(16.0),
child: new AnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
),
);
}
}
/// Keeps a Dart List in sync with an AnimatedList.
///
/// The [insert] and [removeAt] methods apply to both the internal list and the
/// animated list that belongs to [listKey].
///
/// This class only exposes as much of the Dart List API as is needed by the
/// sample app. More list methods are easily added, however methods that mutate the
/// list must make the same changes to the animated list in terms of
/// [AnimatedListState.insertItem] and [AnimatedList.removeItem].
class ListModel<E> {
ListModel({
@required this.listKey,
@required this.removedItemBuilder,
Iterable<E> initialItems,
}) : assert(listKey != null),
assert(removedItemBuilder != null),
_items = new List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final dynamic removedItemBuilder;
final List<E> _items;
AnimatedListState get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
_animatedList.insertItem(index);
}
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
});
}
return removedItem;
}
int get length => _items.length;
E operator [](int index) => _items[index];
int indexOf(E item) => _items.indexOf(item);
}
/// Displays its integer item as 'item N' on a Card whose color is based on
/// the item's value. The text is displayed in bright green if selected is true.
/// This widget's height is based on the animation parameter, it varies
/// from 0 to 128 as the animation varies from 0.0 to 1.0.
class CardItem extends StatelessWidget {
const CardItem({
Key key,
@required this.animation,
this.onTap,
@required this.item,
this.selected: false
}) : assert(animation != null),
assert(item != null && item >= 0),
assert(selected != null),
super(key: key);
final Animation<double> animation;
final VoidCallback onTap;
final int item;
final bool selected;
@override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.display1;
if (selected)
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
return new Padding(
padding: const EdgeInsets.all(2.0),
child: new SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: new SizedBox(
height: 128.0,
child: new Card(
color: Colors.primaries[item % Colors.primaries.length],
child: new Center(
child: new Text('Item $item', style: textStyle),
),
),
),
),
),
);
}
}
void main() {
runApp(new AnimatedListSample());
}
1. 重要的类
- Animation(抽象类)
在一段时间内依次生成一个区间(Tween)之间值的类。
常用类型:
Animation<double>,CurvedAnimation和AnimationController都是Animation<double>类型。
其他类型:
Animation<Color> 、Animation<Size>等。
和UI渲染没有任何关系,用来保存动画的当前值和状态。在动画的每一帧中,可以通过Animation对象的value属性获取动画的当前状态值。
可以通过Animation的以下方法来监听动画每一帧以及执行状态的变化:
1. addListener();给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建。
3. addStatusListener();给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向时会调用。
- Curve曲线
决定Animation对象在整个动画执行过程中输出的值是线性的、曲线的、步进函数或任何曲线函数。
通过CurvedAnimation来指定动画的曲线。
// CurvedAnimation可以通过包装AnimationController和Curve生成一个新的动画对象,将动画和动画执行曲线、控制器关联。
final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
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 用于管理控制动画
具有控制动画的方法:启动forward()、停止stop() 、反向reverse()等方法。会在动画的每一帧生成一个新值。
创建一个Animation对象(但不会启动动画):
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
说明:
1. duration表示动画时长,用来控制动画的速度。
2. 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就不会再触发。
通常会通过with将SingleTickerProviderStateMixin添加到State的定义中,然后将State对象this作为vsync的值。
AnimationController生成数字的区间可以通过lowerBound和upperBound来指定。
默认情况下,在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。
例:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
在动画开始执行后开始生成动画帧,屏幕每刷新一次就是一个动画帧,在动画的每一帧,会随着根据动画的曲线来生成当前的动画值(Animation.value),然后根据当前的动画值去构建UI,当所有动画帧依次触发时,动画值会依次改变,所以构建的UI也会依次变化,所以最终可以看到一个完成的动画。 另外在动画的每一帧,Animation对象会调用其帧监听器,等动画状态发生改变时(如动画结束)会调用状态改变监听器。
注意: 在某些情况下,动画值可能会超出AnimationController的[0.0,1.0]的范围,这取决于具体的曲线。例如,fling()函数可以根据手指滑动(甩出)的速度(velocity)、力量(force)等来模拟一个手指甩出动画,因此它的动画值可以在[0.0,1.0]范围之外 。也就是说,根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。
- Tween
定义从输入范围到输出范围的映射
Tween继承自Animatable<T>,而不是继承自Animation<T>。Animatable中主要定义动画值的映射规则。
要使用Tween对象,需要调用其animate()方法(返回的是一个Animation,而不是Animatable),然后传入一个控制器对象。
例1(在500毫秒内生成从0到255的整数值)
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
例2(构建了一个控制器、一条曲线和一个Tween)
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
// Tween生成[-200.0,0.0]的值
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
// ColorTween将动画输入范围映射为两种颜色值之间过渡输出
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween是一个无状态对象(stateless),构造函数需要begin和end两个参数。输入范围通常为[0.0,1.0],但这不是必须的,可以自定义需要的范围。
默认情况下,AnimationController对象值的范围是[0.0,1.0]。如果需要构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween来添加映射以生成不同的范围或数据类型的值。
Tween对象不存储任何状态,相反,它提供了evaluate(Animation<double> animation)方法,它可以获取动画当前映射值。 Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。
例
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new LogoApp());
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
// 要使用Animation<>对象进行渲染,将Animation对象存储为Widget的成员,然后使用其value值来决定如何绘制
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
// 创建controller
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
// 创建animation对象
animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
// 监听
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
// 开启动画
controller.forward();
}
Widget build(BuildContext context) {
return new AnimatedLogo(animation: animation);
}
dispose() {
// 销毁controller
controller.dispose();
super.dispose();
}
}
// 使用AnimatedWidget简化
class AnimatedLogo extends AnimatedWidget {
// 创建Tween,每一个Tween管理动画的一种效果
static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Opacity(
opacity: _opacityTween.evaluate(animation),
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: _sizeTween.evaluate(animation), // 获取动画的当前值
width: _sizeTween.evaluate(animation),
child: new FlutterLogo(),
),
),
);
}
}
代码存在的一个问题: 更改动画需要更改显示logo的widget。更好的解决方案是将职责分离:
1. 显示logo
2. 定义Animation对象
3. 渲染过渡效果
使用AnimatedBuilder:
_LogoAppState的build方法改为:
Widget build(BuildContext context) {
// Animation对象在_LogoAppState中创建,并在这里传入GrowTransition组件
return new GrowTransition(child: new LogoWidget(), animation: animation);
}
// 1. 显示logo
class LogoWidget extends StatelessWidget {
build(BuildContext context) {
return new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
child: new FlutterLogo(),
);
}
}
// 3. 渲染过渡效果
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),
);
}
}
2. 用AnimatedWidget简化、用AnimatedBuilder重构、监听状态
最基础的动画实现方式
例
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(() {
// 每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,改变Image的宽高,因为它的高度和宽度现在使用的是animation.value ,所以就会逐渐放大。
setState(()=>{});
});
// 启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return new Center(
child: Image.asset("imgs/avatar.png",
width: animation.value,
height: animation.value
),
);
}
// 路由销毁时释放动画资源
dispose() {
// 动画完成时要释放控制器以防止内存泄漏。
controller.dispose();
super.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类封装了调用addListener()和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("imgs/avatar.png",
width: animation.value,
height: animation.value
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
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()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。
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等,很多时候都可以复用这些预置的过渡类。
- 监听动画状态
可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。
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();
}
3. 自定义路由切换动画
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。
如果在Android上也想使用左右切换风格,一个简单的作法是可以直接使用CupertinoPageRoute,如:
Navigator.push(context, CupertinoPageRoute(
builder: (context)=>PageB(),
));
CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件,它实现的就是左右滑动切换。
还可以使用PageRouteBuilder来自定义路由切换动画。
pageBuilder 有一个animation参数,这是Flutter路由管理器提供的,在路由切换时pageBuilder在每个动画帧都会被回调,因此可以通过animation对象来自定义过渡动画。
无论是MaterialPageRoute、CupertinoPageRoute,还是PageRouteBuilder,它们都继承自PageRoute类,而PageRouteBuilder其实只是PageRoute的一个包装,可以直接继承PageRoute类来实现自定义路由,
例(以渐隐渐入动画来实现路由过渡)
PageRouteBuilder方式(优先考虑)
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500), // 动画时间为500毫秒
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return new FadeTransition(
// 使用渐隐渐入过渡,
opacity: animation,
child: PageB(), // 路由B
);
},
),
);
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,
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. 使用FadeRoute
Navigator.push(context, FadeRoute(builder: (context) {
return PageB();
}));
但是有些时候PageRouteBuilder是不能满足需求的,例如在应用过渡动画时需要读取当前路由的一些属性,这时就只能通过继承PageRoute的方式了。
例如只想在打开新路由时应用动画,而在返回时不使用动画,那么在构建过渡动画时就必须判断当前路由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动画(又称:共享元素转换)
把一个组件以动画的方式从一个页面‘传递’到下一个页面(实际上是两个不同的组件,存在关联,显示相同的内容,位置、外观可能不同),能够在两个页面之间建立视觉锚点链接,从而起到引导用户的作用。
实现Hero动画只需要用Hero组件将要共享的widget包装起来,并提供一个相同的tag即可,中间的过渡帧都是Flutter Framework自动完成的。
Hero动画原理:Flutter框架知道新旧路由页中共享元素的位置和大小,所以根据这两个端点在动画执行过程中求出过渡时的插值(中间态)。
例
假设有两个路由A和B,他们的内容交互如下:
A:包含一个用户头像,圆形,点击后跳到B路由,可以查看大图。
B:显示用户头像原图,矩形;
在AB两个路由之间跳转的时候,用户头像会逐渐过渡到目标路由页的头像上
// 路由A
class HeroAnimationRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", // 唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.asset("images/avatar.png",
width: 50.0,
),
),
),
onTap: () {
//打开B路由
Navigator.push(context, PageRouteBuilder(
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return new FadeTransition(
opacity: animation,
child: Scaffold(
appBar: AppBar(
title: Text("原图"),
),
body: HeroAnimationRouteB(),
),
);
})
);
},
),
);
}
}
class HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", // 唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("images/avatar.png"),
),
);
}
}
5. 交织动画(Stagger Animation)
有些时候可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成,比如:有一个柱状图,需要在高度增长的同时改变颜色,等到增长到最大高度后,需要在X轴上平移一段距离。可以发现上述场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画会非常简单。
交织动画需要注意以下几点:
1. 要创建交织动画,需要使用多个动画Animation对象。
2. 一个AnimationController控制所有的动画对象。
3. 给每一个动画对象指定时间间隔
所有动画都由同一个AnimationController驱动,无论动画需要持续多长时间,控制器的值必须在0.0到1.0之间,而每个动画的间隔(Interval)也必须介于0.0和1.0之间。对于在间隔中设置动画的每个属性,需要分别创建一个Tween 用于指定该属性的开始值和结束值。也就是说0.0到1.0代表整个动画过程,可以给不同动画指定不同的起始点和终止点来决定它们的开始时间和终止时间。
例
实现一个柱状图增长的动画:
1. 开始时高度从0增长到300像素,同时颜色由绿色渐变为红色;这个过程占据整个动画时间的60%。
2. 高度增长到300后,开始沿X轴向右平移100像素;这个过程占用整个动画时间的40%。
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({ Key key, this.controller }): super(key: key){
//高度动画
height = Tween<double>(
begin:.0 ,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
color = ColorTween(
begin:Colors.green ,
end:Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0, 0.6,//间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
padding = Tween<EdgeInsets>(
begin:EdgeInsets.only(left: .0),
end:EdgeInsets.only(left: 100.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.6, 1.0, //间隔,后40%的动画时间
curve: Curves.ease,
),
),
);
}
final Animation<double> controller;
Animation<double> height;
Animation<EdgeInsets> padding;
Animation<Color> color;
Widget _buildAnimation(BuildContext context, Widget 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,
);
}
}
class StaggerRoute extends StatefulWidget {
@override
_StaggerRouteState createState() => _StaggerRouteState();
}
class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
}
Future<Null> _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 GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_playAnimation();
},
child: Center(
child: 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切换抽象。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不同,如果类型相同则Key必须不通),旧child会执行隐藏动画,新child会执行执行显示动画。
究竟执行何种动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的builder,定义如下:
typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
该builder在AnimatedSwitcher的child切换时会分别对新、旧child绑定动画:
对旧child,绑定的动画会反向执行(reverse)
对新child,绑定的动画会正向指向(forward)
AnimatedSwitcher的layoutBuilder默认值是AnimatedSwitcher.defaultTransitionBuilder(“渐隐”和“渐显”动画) :
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: 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,
),
),
RaisedButton(
child: const Text('+1',),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
当点击“+1”按钮时,原先的数字会逐渐缩小直至隐藏,而新数字会逐渐放大
AnimatedSwitcher实现原理
要想实现新旧child切换动画,只需要明确两个问题:动画执行的时机是和如何对新旧child执行动画。从AnimatedSwitcher的使用方式可以看到,当child发生变化时(子widget的key和类型不同时相等则认为发生变化),则重新会重新执行build,然后动画开始执行。可以通过继承StatefulWidget来实现AnimatedSwitcher,具体做法是在didUpdateWidget 回调中判断其新旧child是否发生变化,如果发生变化,则对旧child执行反向退场(reverse)动画,对新child执行正向(forward)入场动画
下面是AnimatedSwitcher实现的部分核心伪代码:
Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
if (Widget.canUpdate(widget.child, oldWidget.child)) {
// child没变化,...
} else {
//child发生了变化,构建一个Stack来分别给新旧child执行动画
_widget= Stack(
alignment: Alignment.center,
children:[
//旧child应用FadeTransition
FadeTransition(
opacity: _controllerOldAnimation,
child : oldWidget.child,
),
//新child应用FadeTransition
FadeTransition(
opacity: _controllerNewAnimation,
child : widget.child,
),
]
);
// 给旧child执行反向退场动画
_controllerOldAnimation.reverse();
//给新child执行正向入场动画
_controllerNewAnimation.forward();
}
}
//build方法
Widget build(BuildContext context){
return _widget;
}
AnimatedSwitcher真正的实现比这个复杂,它可以自定义进退场过渡动画以及执行动画时的布局等。
Flutter SDK中还提供了一个AnimatedCrossFade组件,它也可以切换两个子元素,切换过程执行渐隐渐显的动画,和AnimatedSwitcher不同的是AnimatedCrossFade是针对两个子元素,而AnimatedSwitcher是在一个子元素的新旧值之间切换。AnimatedCrossFade实现原理比较简单,也有和AnimatedSwitcher类似的地方.
AnimatedSwitcher高级用法
假设现在我们想实现一个类似路由平移切换的动画:旧页面屏幕中向左侧平移退出,新页面重屏幕右侧平移进入。如果要用AnimatedSwitcher的话,我们很快就会发现一个问题:做不到!我们可能会写出下面的代码:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
AnimatedSwitcher的child切换时会分别对新child执行正向动画(forward),而对旧child执行反向动画(reverse),所以真正的效果便是:新child确实从屏幕右侧平移进入了,但旧child却会从屏幕右侧(而不是左侧)退出。
因为同一个Animation正向(forward)和反向(reverse)是对称的。所以如果我们可以打破这种对称性,那么便可以实现这个功能了,下面我们来封装一个MySlideTransition,它与SlideTransition唯一的不同就是对动画的反向执行进行了定制(从左边滑出隐藏),代码如下:
class MySlideTransition extends AnimatedWidget {
MySlideTransition({
Key key,
@required Animation<Offset> position,
this.transformHitTests = true,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) ;
Animation<Offset> get position => listenable;
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
Offset offset=position.value;
//动画反向执行时,调整x偏移,实现“从左边滑出隐藏”
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
调用时,将SlideTransition替换成MySlideTransition即可:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return MySlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
SlideTransitionX(自定义)
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key key,
@required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) {
// 偏移在内部处理
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
break;
}
}
Animation<double> get position => listenable;
final bool transformHitTests;
final Widget child;
//退场(出)方向
final AxisDirection direction;
Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
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,
);
}
}
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. 动画过渡组件
将在Widget属性发生变化时会执行过渡动画的组件统称为”动画过渡组件“,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。
为了方便使用者可以自定义动画的曲线、执行时长、方向等,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。但是,如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。
- 自定义动画过渡组件
例
实现一个AnimatedDecoratedBox,在decoration属性发生变化时,从旧状态变成新状态的过程可以执行一个过渡动画
class AnimatedDecoratedBox1 extends StatefulWidget {
AnimatedDecoratedBox1({
Key key,
@required this.decoration,
this.child,
this.curve = Curves.linear,
@required this.duration,
this.reverseDuration,
});
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration reverseDuration;
@override
_AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}
class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
with SingleTickerProviderStateMixin {
@protected
AnimationController get controller => _controller;
AnimationController _controller;
Animation<double> get animation => _animation;
Animation<double> _animation;
DecorationTween _tween;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child){
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
_tween = DecorationTween(begin: widget.decoration);
_updateCurve();
}
void _updateCurve() {
if (widget.curve != null)
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
else
_animation = _controller;
}
@override
void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve)
_updateCurve();
_controller.duration = widget.duration;
_controller.reverseDuration = widget.reverseDuration;
if(widget.decoration!= (_tween.end ?? _tween.begin)){
_tween
..begin = _tween.evaluate(_animation)
..end = widget.decoration;
_controller
..value = 0.0
..forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
使用AnimatedDecoratedBox1来实现按钮点击后背景色从蓝色过渡到红色的效果:
Color _decorationColor = Colors.blue;
var duration = Duration(seconds: 1);
...//省略无关代码
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
击后,按钮背景色会从蓝色向红色过渡
上面的代码虽然实现了期望功能,但是代码却比较复杂。AnimationController的管理以及Tween更新部分的代码都是可以抽象出来的,如果这些通用逻辑封装成基类,那么要实现动画过渡组件只需要继承这些基类,然后定制自身不同的代码(比如动画每一帧的构建方法)即可,这样将会简化代码。
为了方便开发者来实现动画过渡组件的封装,Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。
开发者如果要封装动画,只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可
两步
1. 继承ImplicitlyAnimatedWidget类
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear, //动画曲线
@required Duration duration, // 正向动画执行时长
Duration reverseDuration, // 反向动画执行时长
}) : super(
key: key,
curve: curve,
duration: duration,
reverseDuration: reverseDuration,
);
final BoxDecoration decoration;
final Widget child;
@override
_AnimatedDecoratedBoxState createState() {
return _AnimatedDecoratedBoxState();
}
}
2. State类继承自AnimatedWidgetBaseState(该类继承自ImplicitlyAnimatedWidgetState类)。
class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
DecorationTween _decoration; //定义一个Tween
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(visitor) {
// 在需要更新Tween时,基类会调用此方法
_decoration = visitor(_decoration, widget.decoration,
(value) => DecorationTween(begin: value));
}
}
实现了build和forEachTween两个方法。在动画执行过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build方法中我们需要构建每一帧的DecoratedBox状态,因此得算出每一帧的decoration 状态,这个我们可以通过_decoration.evaluate(animation) 来算出,其中animation是ImplicitlyAnimatedWidgetState基类中定义的对象,_decoration是我们自定义的一个DecorationTween类型的对象,那么现在的问题就是它是在什么时候被赋值的呢?要回答这个问题,我们就得搞清楚什么时候需要对_decoration赋值。我们知道_decoration是一个Tween,而Tween的主要职责就是定义动画的起始状态(begin)和终止状态(end)。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有以下两种情况:
1. AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,即_decoration值为DecorationTween(begin: decoration) 。
2. AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin: _decoration.animate(animation),end:decoration)。
现在forEachTween的作用就很明显了,它正是用于来更新Tween的初始值的,在上述两种情况下会被调用,而开发者只需重写此方法,并在此方法中更新Tween的起始状态值即可。而一些更新的逻辑被屏蔽在了visitor回调,我们只需要调用它并给它传递正确的参数即可,visitor方法签名如下:
Tween visitor(
Tween<dynamic> tween, //当前的tween,第一次调用为null
dynamic targetValue, // 终止状态
TweenConstructor<dynamic> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
动画过渡组件的反向动画
在使用动画过渡组件,只需要在改变一些属性值后重新build组件即可,所以要实现状态反向过渡,只需要将前后状态值互换即可实现。
另一种方式(尽量避免使用):
ImplicitlyAnimatedWidget构造函数中有一个reverseDuration属性用于设置反向动画的执行时长。如果要让reverseDuration生效,只能先获取controller,然后再通过controller.reverse()来启动反向动画。
在上面示例的基础上实现一个循环的点击背景颜色变换效果,要求从蓝色变为红色时动画执行时间为400ms,从红变蓝为2s,如果要使reverseDuration生效需要这么做:
AnimatedDecoratedBox(
duration: Duration( milliseconds: 400),
decoration: BoxDecoration(color: _decorationColor),
reverseDuration: Duration(seconds: 2),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
if (_decorationColor == Colors.red) {
ImplicitlyAnimatedWidgetState _state =
context.findAncestorStateOfType<ImplicitlyAnimatedWidgetState>();
// 通过controller来启动反向动画
_state.controller.reverse().then((e) {
// 经验证必须调用setState来触发rebuild,否则状态同步会有问题
setState(() {
_decorationColor = Colors.blue;
});
});
} else {
setState(() {
_decorationColor = Colors.red;
});
}
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
上面的代码实际上是非常糟糕且没必要的,它需要了解ImplicitlyAnimatedWidgetState内部实现,并且要手动去启动反向动画。
完全可以通过如下代码实现相同的效果:
AnimatedDecoratedBox(
duration: Duration(
milliseconds: _decorationColor == Colors.red ? 400 : 2000),
decoration: BoxDecoration(color: _decorationColor),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
setState(() {
_decorationColor = _decorationColor == Colors.blue
? Colors.red
: Colors.blue;
});
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
为什么ImplicitlyAnimatedWidgetState要提供一个reverseDuration参数呢?该参数并非是给ImplicitlyAnimatedWidgetState用的,而是给子类用的!
要使reverseDuration 有用就必须得获取controller 属性来手动启动反向动画,ImplicitlyAnimatedWidgetState中的controller 属性是一个保护属性,定义如下:
@protected
AnimationController get controller => _controller;
而保护属性原则上只应该在子类中使用,而不应该像上面示例代码一样在外部使用。
可以得出两条结论:
1. 使用动画过渡组件时如果需要执行反向动画的场景,应尽量使用状态互换的方法,而不应该通过获取ImplicitlyAnimatedWidgetState中controller的方式。
2. 如果自定义的动画过渡组件用不到reverseDuration ,那么最好就不要暴露此参数,比如上面自定义的AnimatedDecoratedBox定义中就可以去除reverseDuration 可选参数,如:
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear,
@required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
Flutter预置的动画过渡组件
Flutter SDK中预置了很多动画过渡组件,实现方式和AnimatedDecoratedBox差不多。
1. AnimatedPadding
在padding发生变化时会执行过渡动画到新状态
2. AnimatedPositioned
配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态。
3. AnimatedOpacity
在透明度opacity发生变化时执行过渡动画到新状态
4. AnimatedAlign
当alignment发生变化时会执行过渡动画到新的状态。
5. AnimatedContainer
当Container属性发生变化时会执行过渡动画到新的状态。
6. AnimatedDefaultTextStyle
当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式。
例
import 'package:flutter/material.dart';
class AnimatedWidgetsTest extends StatefulWidget {
@override
_AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
}
class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
double _padding = 10;
var _align = Alignment.topRight;
double _height = 100;
double _left = 0;
Color _color = Colors.red;
TextStyle _style = TextStyle(color: Colors.black);
Color _decorationColor = Colors.blue;
@override
Widget build(BuildContext context) {
var duration = Duration(seconds: 5);
return SingleChildScrollView(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {
_padding = 20;
});
},
child: AnimatedPadding(
duration: duration,
padding: EdgeInsets.all(_padding),
child: Text("AnimatedPadding"),
),
),
SizedBox(
height: 50,
child: Stack(
children: <Widget>[
AnimatedPositioned(
duration: duration,
left: _left,
child: RaisedButton(
onPressed: () {
setState(() {
_left = 100;
});
},
child: Text("AnimatedPositioned"),
),
)
],
),
),
Container(
height: 100,
color: Colors.grey,
child: AnimatedAlign(
duration: duration,
alignment: _align,
child: RaisedButton(
onPressed: () {
setState(() {
_align = Alignment.center;
});
},
child: Text("AnimatedAlign"),
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
color: _color,
child: FlatButton(
onPressed: () {
setState(() {
_height = 150;
_color = Colors.blue;
});
},
child: Text(
"AnimatedContainer",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDefaultTextStyle(
child: GestureDetector(
child: Text("hello world"),
onTap: () {
setState(() {
_style = TextStyle(
color: Colors.blue,
decorationStyle: TextDecorationStyle.solid,
decorationColor: Colors.blue,
);
});
},
),
style: _style,
duration: duration,
),
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
].map((e) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: e,
);
}).toList(),
),
);
}
}