android 里面我们就可以根据 layout 的变化设计自己的 latyout 切换动画,甚至矢量动画还可以进行 path 方面的无缝切换,比如从圆形自然过度到矩形
Flutter 这里自然也响应提供了相关动画,但是区别肯定还是有的,首先 Flutter 这里名字就改了叫做:隐式动画,我想说何必给 coder 找不自在呢,延续 android 的传统不好嘛~
AnimatedSwitcher- widget 内容改变时可以播放自己指定的动画AnimatedContainer- 带动画的 Container,像 Container 一眼使用,在其中 color、width、height、圆角改变时会触发过度动画,动画不能控制,有些类似与 path 动画AnimatedCrossFade- 切换不同布局时可以显示动画,但是不能自己设置动画,默认就是淡入淡出,并且在大小不通切换时显示不好DecoratedBoxTransition- 边框动画,核心是通过圆角角度的改变实现形状上的变化,这个变化是自然过度的,这点和 path 动画是一样了AnimatedDefaultTextStyle- 文字样式改变时的切换动画,主要呈现的大小变换方面的动画,颜色的渐变过度不明显,但是体验不好的地方在于,大小字切换时字体粗细的变化真实有点辣眼,有点卡顿AnimatedModalBarrier- 颜色改变的变换动画,特殊的地方在于其必须放到所操的 widget 的 child 中,有明确的应用场景,就是点击时改变背景色,比如 dialog 弹出时,背景变灰色AnimatedOpacity- 透明度的变化动画AnimatedPhysicalModel- 阴影变换动画,设置有些复杂AnimatedPositioned- stack 中 widget 位置,大小变换动画AnimatedSize- widget 大小变换动画
AnimatedContainer
AnimatedContainer 顾名思义就是带动画的 Container,属性设置使用和 Container 时一模一样的,区别就是可以设置动画时间和插值器
动画效果这块和 矢量动画类似,都可以实现前后状态间的无缝切换,由系统完成动画每帧的数值输出。但是能做到 矢量动画 那种效果的属性只有:color、width、height、圆角,其他都不行,比如图片切换就是一下就切换了,shape 形状的切换,比如放大从圆形到矩形,圆形在达到最大时一瞬间会切换到矩形,矩形再慢慢放大
AnimatedContainer 的使用套路就是,把属性值写在外部,通过 setState 改变属性值就可以实现切换动画了
这方面看我写的例子:
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
double width = 50;
double height = 50;
Color color = Colors.blueAccent;
BoxShape shape = BoxShape.circle;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedContainer(
duration: Duration(seconds: 1),
width: width,
height: height,
decoration: BoxDecoration(
color: color,
shape: shape,
),
),
RaisedButton(
child: Text("AAA"),
onPressed: () {
setState(() {
width = 300;
height = 300;
color = Colors.pink;
shape = BoxShape.rectangle;
});
},
),
],
);
}
}
官方文档这里给出了一个非常好的例子,AnimatedContainer 能实现的极限都在这里面了,其中形状的改变是通过改变圆角矩形的角度实现的:BorderRadius.circular(8);
class TestWidgetState extends State<TestWidget>
with SingleTickerProviderStateMixin {
double _width = 50;
double _height = 50;
Color _color = Colors.green;
BorderRadiusGeometry _borderRadius = BorderRadius.circular(8);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: AnimatedContainer(
// Use the properties stored in the State class.
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
// Define how long the animation should take.
duration: Duration(seconds: 1),
// Provide an optional curve to make the animation feel smoother.
curve: Curves.fastOutSlowIn,
),
),
RaisedButton(
child: Icon(Icons.play_arrow),
// When the user taps the button
onPressed: () {
// Use setState to rebuild the widget with new values.
setState(() {
// Create a random number generator.
final random = Random();
// Generate a random width and height.
_width = random.nextInt(300).toDouble();
_height = random.nextInt(300).toDouble();
// Generate a random color.
_color = Color.fromRGBO(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
1,
);
// Generate a random border radius.
_borderRadius =
BorderRadius.circular(random.nextInt(100).toDouble());
});
},
),
],
);
}
}
最后大家注意啊,AnimatedContainer 动画只能直接操作 Container 本身的属性,child 里的子 widget 就管不了了
AnimatedSwitcher
基本使用
AnimatedSwitcher 是 Flutter 中提供的用于 widget 切换内容时得动画样式,目前看到只能支持同一个 widget 得内容变化,切换不同类型的 widget 还在研究
AnimatedSwitcher 属性有几个:
child- 内容切换动画作用于得 widgetduration- 动画从 A -> B 的时间reverseDuration- 动画反过来从 B -> A 的时间switchInCurve- 动画从 A -> B 的动画插值器,Curves.linearswitchOutCurve- 反过来得插值器transitionBuilder- 动画layoutBuilder- 包装新旧 Widget 的组件,默认是一个 Stack
其中注意点是 key,flutter widget 树自身有缓存的,同一个 widget 只是内容更新的话是不会重建该 widget 的,但是 AnimatedSwitcher 想要有动画出来,必须是2个 widget 才行的,所以我们要手动设置 key 以规避 Flutter 中的 widget 缓存机制
网上这部分内容基本都出自官方文档:9.6 通用"切换动画"组件(AnimatedSwitcher)
官方文档没 gif,看不出效果,这里我帖一下效果和代码:
class TestWidgetState extends State<TestWidget> {
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.display1,
),
),
RaisedButton(
child: const Text('+1',),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
然后我们再来一个 icon 切换的例子
class TestWidgetState extends State<TestWidget> {
IconData _actionIcon = Icons.delete;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
transitionBuilder: (child, anim) {
return ScaleTransition(child: child, scale: anim);
},
duration: Duration(milliseconds: 200),
child: IconButton(
key: ValueKey(_actionIcon),
icon: Icon(_actionIcon),
),
),
RaisedButton(
child: Text("切换图标"),
onPressed: () {
setState(() {
if (_actionIcon == Icons.delete)
_actionIcon = Icons.done;
else
_actionIcon = Icons.delete;
});
},
),
],
),
);
}
}
AnimatedSwitcher 原理
其实原理很简单,一说就明白。因为 AnimatedSwitcher 其中的 child widget 的key 不一样,那么每次 child widget 内容变化时都会被认为是有新的 widget 出现,会重新 build,我们在 didUpdateWidget 中拿到新久 widget,对旧 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();
}
}
AnimatedSwitcher 高级使用
AnimatedSwitcher 的特征大家应该都明白了,新旧 widget 之间会执行一个动画的前进和反转,奇特证就是从哪里来旧回到哪里,比如新文字从右边进来,那么老的文字就从右边出去,总体上动画的执行必须是顺序的
那么我们可以实现自己想要的效果嘛,比如右边进,左边出。其实这样是可以的,AnimatedSwitcher 我们也不用改,我们可以改一改动画API FlideTransition,所有的动画都是在 AnimationWidget 基础上写的
针对这个需求,我们仿照 FlideTransition 内部实现,把动画在反转时把 X 轴的值加个-号就是我们要的效果啦,大多数时候我们都是用这种思路实现的
这个例子是官方文档上面的,代码上我多少改了一下,主要是在使用上更方便一点,封装度高了一点,针对 X/Y 轴做了封装
- 这是对 SlideTransition 的改造,内部试用了 SlideTransition 的原始实现,核心就是在动画执行反转时对 Offset 坐标数据进行响应的处理,记住这个套路,其他的也一样
enum FreeSlideTransitionMode {
reverseX,
reverseY,
}
class FreeSlideTransition extends AnimatedWidget {
Animation<Offset> animation;
var child;
Offset begin;
Offset end;
FreeSlideTransitionMode type;
// x,y 轴反转播放时不同的数据处理,用 map 承载
Map<FreeSlideTransitionMode, Function(Animation animation, Offset offset)> worksMap = {
// x 轴反转操作,典型应用,右进左出
FreeSlideTransitionMode.reverseX: (Animation animation, Offset offset) {
if (animation.status == AnimationStatus.reverse) {
return offset = Offset(-offset.dx, offset.dy);
}
return offset;
},
FreeSlideTransitionMode.reverseY: (Animation animation, Offset offset) {
if (animation.status == AnimationStatus.reverse) {
return offset = Offset(offset.dx, -offset.dy);
}
return offset;
},
};
// 构造方法的多态,写着有点麻烦,看起来也不省事
FreeSlideTransition(this.type, this.child,
{Key key, Animation<double> animation, Offset begin, Offset end})
: super(key: key, listenable: animation) {
this.animation = Tween<Offset>(begin: begin, end: end).animate(animation);
}
FreeSlideTransition.reverseX(
{Widget child,
Animation<double> animation,
Key key,
Offset begin,
Offset end})
: this(FreeSlideTransitionMode.reverseX, child,
key: key, animation: animation, begin: begin, end: end);
FreeSlideTransition.reverseY(
{Widget child,
Animation<double> animation,
Key key,
Offset begin,
Offset end})
: this(FreeSlideTransitionMode.reverseY, child,
key: key, animation: animation, begin: begin, end: end);
@override
Widget build(BuildContext context) {
var offset = animation.value;
offset = worksMap[type](animation, offset);
// SlideTranslation 内部就是这么实现的
return FractionalTranslation(
translation: offset,
child: child,
);
}
}
- 具体试用:在 Column 中有时候位置会失效,中间加上一个 padding 过度就好了
class Test3 extends State<TestWidget> {
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 FreeSlideTransition.reverseY(
animation: animation,
begin: Offset(0, 1),
end: Offset(0, 0),
child: child,
);
},
child: Text(
'$_count',
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
key: ValueKey<int>(_count),
style: Theme.of(context).textTheme.display1,
),
),
// Padding(
// padding: EdgeInsets.all(20.0),
// ),
// Text("AAA"),
RaisedButton(
child: Text(
'+1',
),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
好了就是这样,有的朋友可能看官方文档时挺简单的,但是还是推荐大家自己写一下,文档的代码封装度不够,也有些繁琐,自己试试也能练习一下功能封装不是,至少我这个 FreeSlideTransition 是可以放到 lib 库里面的
有的朋友不理解为什么还要在 AnimatedSwitcher 里面自己写 tween 呢。其实一开始我也是不理解的,后来一想啊,AnimationControl 的数值默认是 [0-1] 的,我们要是想用自己的数据设置,可不就得自己写 Tween 啊
AnimatedCrossFade
AnimatedCrossFade 不同布局切换时可以显示动画,但是不能自己设置动画,默认就是淡入淡出,并且在大小不通切换时显示不好
我走一个 gif,让大家看看 widget 在大小不同切换时那种强烈的突兀感,小变大还行,但是大变小就不行了
AnimatedCrossFade 可惜不能自己设置动画,默认也就是个渐变动画,限制挺大的
代码上呢,需要我们指定显示 frist widget 还是 second widget,我们在 widget 外部写一个标记,setState 改变这个标记就能触发动画了
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
var isFristShow = true;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedCrossFade(
firstChild: Container(
alignment: Alignment.center,
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blueAccent,
),
child: Text("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
),
secondChild: Container(
alignment: Alignment.center,
width: 300,
height: 300,
child: Text("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
decoration:
BoxDecoration(shape: BoxShape.rectangle, color: Colors.pinkAccent),
),
crossFadeState: isFristShow
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
),
RaisedButton(
child: Text("AAA"),
onPressed: () {
setState(() {
isFristShow = !isFristShow;
});
},
),
],
);
}
}
DecoratedBoxTransition
DecoratedBoxTransition 是边框变化动画,只能进行边框变化的动画,但他是我们一直所追求的,他能实现 widget 形状变化的自然过度,当然和我们说 AnimatedContainer 时一样,起形状自然过度依然还是依靠圆角矩形的圆角度数实现的
DecoratedBoxTransition 属性:
child-decoration- 表示由外传递进来的动画属性值的变化,通过获取其值,填充到child的边框上position- 表示边框动画的位置,可以是前景位置或者是背景位置,前景位置会盖住child元素
DecoratedBoxTransition 的 child 不用设置背景,DecorationTween 数值生成器会自动给 child 加上设定的 shape 背景的
_animation = DecorationTween(
begin: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(0.0)),
color: Colors.red,
),
end: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(100.0)),
color: Colors.green,
),
)
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
Animation<Decoration> _animation;
AnimationController _controller;
Animation _curve;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_curve = CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
_animation = DecorationTween(
begin: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(0.0)),
color: Colors.red,
),
end: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(100.0)),
color: Colors.green,
),
).animate(_curve)
..addStatusListener((AnimationStatus state) {
if (state == AnimationStatus.completed) {
_controller.reverse();
} else if (state == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: <Widget>[
DecoratedBoxTransition(
position: DecorationPosition.background,
decoration: _animation,
child: Container(
child: Container(
padding: EdgeInsets.all(50),
child: Text("AAAAAA"),
)),
)
],
);
}
}
AnimatedDefaultTextStyle
AnimatedDefaultTextStyle 文字样式改变时的切换动画,主要呈现的大小变换方面的动画,颜色的渐变过度不明显,但是体验不好的地方在于,大小字切换时字体粗细的变化真实有点辣眼,尤其是文字字号大的时候
吐槽一下,Google 团队的代码质量也在下降啊,在前有 AnimatedContainer 的前提下,AnimatedDefaultTextStyle 和 AnimatedContainer 的设计,使用,命名套路完全不一样,Google 你是要闹哪样,有代码质量审查吗?AnimatedDefaultTextStyle 和 AnimatedContainer 他们两者其实是一种东西,但是开发者之间没有沟通,搞出2种东西,真是用着蛋疼啊
AnimatedDefaultTextStyle 使用上作为 text 的外层 widget 来用的,其实搞成 AnimatedContainer 那样直接代替 text 多好,其实 text 那些属性大多数 AnimatedDefaultTextStyle 也有,搞得不乱不类的,真让人火大
动画的触发一样还是通过外层变量控制,通过 staState 来触发
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
var _isSelected = true;
var info1 = "Flutter !!!";
var info2 = "is not you !!!";
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
AnimatedDefaultTextStyle(
softWrap: false,
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
curve: Curves.linear,
duration: Duration(milliseconds: 300),
child: Text( info2),
style: _isSelected
? TextStyle(
fontSize: 10.0,
color: Colors.red,
fontWeight: FontWeight.bold,
)
: TextStyle(
fontSize: 30.0,
color: Colors.black,
fontWeight: FontWeight.w300,
),
),
RaisedButton(
child: Text("AA"),
onPressed: () {
setState(() {
_isSelected = !_isSelected;
});
},
),
],
);
}
}
文字若是切换前后行数不一样的话,动画就比较难看了,大家看下面这个例子,在用的时候大家切记一行变多行
AnimatedModalBarrier
AnimatedModalBarrier颜色改变的变换动画,特殊的地方在于其必须放到所操的 widget 的 child 中,有明确的应用场景,就是点击时改变背景色,比如 dialog 弹出时,背景变灰色
AnimatedModalBarrier 有几个参数,除了 color 外具体有啥用我也不知道:
color- 颜色值动画变化dismissible- 是否触摸当前ModalBarrier将弹出当前路由,配合点击事件弹出路由使用semanticsLabel- 语义化标签barrierSemanticsDismissible- 语义树中是否包括ModalBarrier语义
color 这里接收一个 animation 动画对象,这样的话我们可以自己设置前后颜色值,时长,添加控制等,API 自由度比其他同类变换动画 API 强多了
下面的例子里我设置了一个循环播放
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _curve;
Animation<Color> animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)
..addStatusListener((AnimationStatus state) {
if (state == AnimationStatus.completed) {
_controller.reverse();
} else if (state == AnimationStatus.dismissed) {
_controller.forward();
}
});
animation = ColorTween(
begin: Colors.blue,
end: Colors.pinkAccent,
).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
Container(
width: 300,
height: 300,
child: AnimatedModalBarrier(
semanticsLabel: "StackBarrier",
barrierSemanticsDismissible: true,
dismissible: true,
color: animation,
),
),
Positioned(
left: 20,
top: 20,
child: RaisedButton(
child: Text("AA"),
onPressed: () {
_controller.forward();
},
),
),
],
);
}
}
AnimatedOpacity
AnimatedOpacity 透明度的变化动画,没什么可说的,看代码就是,下面我把颜色变换的动画一起加进来
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> animation;
double opacity = 1.0;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
animation = ColorTween(
begin: Colors.blue,
end: Colors.pinkAccent,
).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
AnimatedOpacity(
curve: Curves.fastOutSlowIn,
opacity: opacity,
duration: Duration(seconds: 1),
child: Container(
width: 300,
height: 300,
child: AnimatedModalBarrier(
semanticsLabel: "StackBarrier",
barrierSemanticsDismissible: true,
dismissible: true,
color: animation,
),
),
),
Positioned(
left: 20,
top: 20,
child: RaisedButton(
child: Text("AA"),
onPressed: () {
setState(() {
opacity = 0.3;
_controller.forward();
});
},
),
),
],
);
}
}
AnimatedPhysicalModel
AnimatedPhysicalModel 属性如下,有点多,大家仔细看,看不懂的看下面 demo 就行:
shape:阴影的形状clipBehavior:阴影的裁剪方式Clip.none:无模式Clip.hardEdge:裁剪速度稍快,但容易失真,有锯齿Clip.antiAlias:裁剪边缘抗锯齿,使得裁剪更平滑,这种模式裁剪速度比antiAliasWithSaveLayer快,但是比hardEdge慢Clip.antiAliasWithSaveLayer:裁剪后具有抗锯齿特性并分配屏幕缓冲区,所有后续操作在缓冲区进行
borderRadius:背景的边框elevation:阴影颜色值的深度color:背景色animateColor:背景色是否用动画形式展示shadowColor:阴影的动画值animateShadowColor:阴影是否用动画形式展示
看着很多,但是大家不要懵,其实就一个有用,就是 shadowColor,我们改 shadowColor 就会触发动画,不过 color 这个属性必须设置,要不报错
整体动画来说,我是真不知道这个动画应用在哪里,是不是像上面那个一样,做点击后背景色的变化呢,谁知道呢...
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
var isShadow = true;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
fit: StackFit.loose,
children: <Widget>[
AnimatedPhysicalModel(
curve: Curves.fastOutSlowIn,
color: Colors.grey.withOpacity(0.2),
clipBehavior: Clip.antiAliasWithSaveLayer,
borderRadius: BorderRadius.circular(12.0),
animateColor: true,
animateShadowColor: true,
shape: BoxShape.rectangle,
shadowColor: isShadow ? _shadowColor1 : _shadowColor2,
elevation: 5.0,
duration: Duration(milliseconds: 300),
child: Container(
width: 200,
height: 200,
child: Text("AA"),
),
),
Positioned(
left: 20,
top: 20,
child: RaisedButton(
child: Text("AA"),
onPressed: () {
setState(() {
isShadow = !isShadow;
});
},
),
),
],
);
}
}
AnimatedPositioned
AnimatedPositioned 这个大家看名字就知道了,就是严格按照之前的 API 命名的,这样才好嘛,这样才会一看就知道是干啥的,这里再次 diss 一下其他垃圾的起名和设计
这个我不想说了,大家看代码都清楚
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
var isPosition = true;
var top1 = 20.0;
var left1 = 20.0;
var width1 = 200.0;
var height1 = 200.0;
var top2 = 100.0;
var left2 = 100.0;
var width2 = 300.0;
var height2 = 300.0;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
AnimatedPositioned(
top: isPosition ? top1 : top2,
left: isPosition ? left1 : left2,
width: isPosition ? width1 : width2,
height: isPosition ? height1 : height2,
child: Container(
color: Colors.blueAccent,
),
duration: Duration(milliseconds: 300),
),
Positioned(
left: 20,
top: 20,
child: RaisedButton(
child: Text("AA"),
onPressed: () {
setState(() {
isPosition = !isPosition;
});
},
),
),
],
);
}
}
AnimatedSize
上面看了这么多了,到这大家啥套路都知道了吧,就是效果不是很满意,看下面的 gif,大家能看到若是因为 widget 的大小变化而造成 widget 有位移的话,那么会进行位移动画,而大小就没动画了,这点挺不爽的
class Test3 extends State<TestWidget> with SingleTickerProviderStateMixin {
double size1 = 200;
double size2 = 300;
var isSize = true;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text("AA"),
onPressed: () {
setState(() {
isSize = !isSize;
});
},
),
AnimatedSize(
alignment: Alignment.center,
curve: Curves.fastOutSlowIn,
vsync: this,
duration: Duration(seconds: 1),
reverseDuration: Duration(seconds: 2),
child: Container(
width: isSize ? size1 : size2,
height: isSize ? size1 : size2,
color: Colors.blueAccent,
),
),
],
);
}
}