动画从原理上可以分为两类:补间动画和基于物理动画。
补间动画顾名思义就是介于两点之间,两点也就是起点和终点。在补间动画中,定义了起点和终点以及时间轴,再定义过渡时间和速度的曲线。然后框架会计算如何从起点过渡到终点。
物理动画是基于对真实世界的行为模拟来进行建模的。像乒乓球的落地和弹起等,
在flutter中,动画又被区分隐式动画、显式动画、hexo动画、交织动画,物理动画等。下面详细解释。
隐式动画的使用
先看效果:
实现一个盒子缩放,点击按钮放大:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("隐式动画"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
_updateState();
},
child: Text('Animate'),
),
Container(
width: _bigger ? 400 : 100,
height: _bigger ? 400 : 100,
color: Colors.lightBlue[200],
child: Center(
child: Text(
'Animatiaon',
style: Theme.of(context).textTheme.subtitle1,
),
),
),
],
),
),
);
}
没有任何动画,画面突兀生硬,下面我们用隐式动画实现一个柔和的效果:
- 把Container替换为AnimatedContainer;
- 设置动画时长为400毫秒;
- 设置动画曲线;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("隐式动画"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
_updateState();
},
child: Text('Animate'),
),
AnimatedContainer(
duration: Duration(
milliseconds: 400,
),
width: _bigger ? 400 : 100,
height: _bigger ? 400 : 100,
curve: Curves.bounceOut,
color: Colors.lightBlue[200],
child: Center(
child: Text(
'Animatiaon',
style: Theme.of(context).textTheme.subtitle1,
),
),
),
],
),
),
);
}
简单的3步,就实现了一个缩放动画。能这么简单,是因为flutter帮我妈实现了动画细节。查看AnimatedContainer,可以看到它继承自ImplicitlyAnimatedWidget:
class AnimatedContainer extends ImplicitlyAnimatedWidget
ImplicitlyAnimatedWidgets:AnimatedContainer是Flutter的动画库为我们实现的管理动画的小部件。这些小部件统称为隐式动画或隐式动画小部件,它们的名称来自于ImplicitlyAnimatedWidget,也就是它们实现的父类,下面列举下常用的小部件:
ALign->AnimatedAlign
Container->AnimatedContainer
DefaultTextStyle->AnimatedDefaultTextStyle
Opacity->AnimatedOpacity
Padding->AnimatedPadding
PhysicalModel->AnimatedPhysicalModel
Positioned->AnimatedPositioned
PositionedDirectional->AnimatedPositionedDirectional
Theme->AnimatedThemeSize->AnimatedSize
这些小部件在首次添加到widget树时将不进行动画处理,也就是我们进入页面的时候,是没有动画的。但是当我们更改其属性时,它们将通过对指定持续时间内的变化自动进行动画处理来响应这些变化。怎么实现自动呢,是因为ImplicitlyAnimatedWidgetState在内部创建并管理AnimationController来为动画提供动力。
当然实现起来简单也就意味着动画效果简单,ImplicitlyAnimatedWidgets及其子类受到一些限制:除了动画属性之外,开发人员只能为动画选择持续时间和曲线。如果需要对动画进行更多控制(例如,将其停在中间的某个位置),ImplicitlyAnimatedWidgets并不能办到,这时候我们就需要使用显式动画。
Tween动画的使用
上面了解了基本的隐式动画,但是一些widget没有的属性,比如颜色变化等,我们就需要用Tween动画实现,它相当于简单自定义的隐式动画。
下面用一个案例实现P图软件的调色滤镜效果,给我的女朋友调个色。我相信你学会这一招,一定能讨得女朋友欢心,前提是你先有个女朋友。
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Stack(
children: <Widget>[
Image.asset(
R.bg,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
fit: BoxFit.fitWidth,
),
Column(
children: <Widget>[
Center(
child: Image.asset(
R.lihuili,
),
),
Slider.adaptive(
value: _sliderValue,
onChanged: (double value) {
setState(() {
_sliderValue = value;
_newColor =
Color.lerp(Colors.white, Colors.blue, _sliderValue);
});
})
],
),
],
),
);
}
先实现布局,然后加入TweenAnimationBuilder:
Center(
child: TweenAnimationBuilder(
tween: ColorTween(begin: Colors.white,end: Colors.green),
duration: Duration(milliseconds: 300),
child: Image.asset(
R.lihuili,
),
),
)
因为是滤镜,所以使用ColorTween实现。然后把end颜色改为拖动手柄产生的值:
tween: ColorTween(begin: Colors.white, end: _newColor),
给图片加上颜色过滤:
ColorFiltered(
child: Image.asset(
R.lihuili,
),
colorFilter: ColorFilter.mode(color, BlendMode.modulate),
);
见证奇迹的时刻来了:
没几行代码,就实现了一个滤镜效果。嗯,加鸡腿。。。
Animation
了解了一些动画,下面介绍下动画的核心类:
Animation,Flutter 动画库中的核心类,插入用于指导动画的值。
Animation 对象知道动画目前的状态(例如,是否开始,暂停,前进或倒退),但是对屏幕上显示的内容一无所知。AnimationController 管理 Animation。
AnimationController 是个特殊的 Animation 对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController 在给定期间内会线性生成从 0.0 到 1.0 的数字。CurvedAnimation 定义动画在开始值和结束值之间如何变化的路径或曲线。
Duration 动画花费的时间。
Tween 为动画对象插入一个范围值。例如,Tween 可以定义插入值由红到蓝,或从 0 到 255。
在默认情况下,AnimationController 对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween 配置动画来插入不同的范围或数据类型。Tween.animate,要使用 Tween 对象,需要 Tween 调用 animate(),传入控制器对象。
使用 Listeners 和 StatusListeners 监视动画状态变化。
一个 Animation 对象可以有不止一个 Listener 和 StatusListener,用 addListener() 和 addStatusListener() 来定义。当动画值改变时调用 Listener。Listener 最常用的操作是调用 setState() 进行重建。当一个动画开始,结束,前进或后退时,会调用 StatusListener,用 AnimationStatus 来定义。Ticker 动画定时器。
AnimationController 的vsync对象会绑定一个ticker,当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在前台显示时依然运行消耗资源。 如果要使用自定义的State对象作为vsync时,混入TickerProviderStateMixin。就不需要我们自己释放资源了。
显式动画
上面简单的动画不满足我们的时候,就需要自己控制动画了。
flutter 为我们提供的switch 不能改变大小,满足不了我们的需要,下面我们自己实现一个,首先分析都需要哪些属性:宽高、打开的颜色、关闭的颜色、按钮的颜色、打开关闭的事件。
class CustomSwitch extends StatefulWidget {
CustomSwitch({
Key key,
this.width = 120,
this.height = 50,
this.activeColor = Colors.blue,
this.inactiveColor = Colors.grey,
this.buttonColor = Colors.white,
this.onChanged,
this.value = false,
}) : super(key: key);
final double width;
final double height;
/// 打开时的颜色
final Color activeColor;
/// 关闭时的颜色
final Color inactiveColor;
/// 按钮颜色
final Color buttonColor;
final ValueChanged<bool> onChanged;
final bool value;
@override
_CustomSwitchState createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch> {
bool value;
double paddingValue ;
double diameter;
@override
void initState() {
super.initState();
value = widget.value;
paddingValue=widget.height/12;
diameter = widget.height - 2 * paddingValue;
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: value ? widget.activeColor : widget.inactiveColor,
borderRadius: BorderRadius.circular(widget.height / 2),
),
padding: EdgeInsets.all(paddingValue),
child: Align(
alignment: value?Alignment.centerRight:Alignment.centerLeft,
child: Container(
width: diameter,
height: diameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.buttonColor,
),
),
),
);
}
}
先实现ui,效果如下:
- 我们要创建一个动画,当点击的时候,滑块会从左边滑动到右边,所以首先混入 SingleTickerProviderStateMixin ,然后声明动画:
Animation<Alignment> _animation;
AnimationController _animationController;
- 初始化:
// 设置动画取值范围和时间曲线
_animation = Tween<Alignment>(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
)
);
- 在我们的布局外面添加AnimatedBuilder:
return AnimatedBuilder(
animation: _animationController,
builder: (animation,child){
return Container(
...
- 修改滑块位置为动画的值:
child: Align(
alignment: _animation.value,
child: Container(
...
- 万事俱备,下面就是点击滑块了,如果动画结束了,也就是从左边滑到了右边(初始为左边),或者从右边滑到了左边(初始为右),那么要反向运动,也就是reverse,否则就是开始动画,也就是forward:
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
_value = !_value;
widget.onChanged?.call(_value);
},
...
看下效果:
完整代码:
import 'package:flutter/material.dart';
class CustomSwitch extends StatefulWidget {
CustomSwitch({
Key key,
this.width = 120,
this.height = 50,
this.activeColor = Colors.blue,
this.inactiveColor = Colors.grey,
this.buttonColor = Colors.white,
this.onChanged,
this.value = false,
}) : super(key: key);
final double width;
final double height;
/// 打开时的颜色
final Color activeColor;
/// 关闭时的颜色
final Color inactiveColor;
/// 按钮颜色
final Color buttonColor;
final ValueChanged<bool> onChanged;
final bool value;
@override
_CustomSwitchState createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch>
with SingleTickerProviderStateMixin {
bool _value;
double _paddingValue;
double _diameter;
Animation<Alignment> _animation;
AnimationController _animationController;
@override
void initState() {
super.initState();
_value = widget.value;
_paddingValue = widget.height / 12;
_diameter = widget.height - 2 * _paddingValue;
// 初始化动画控制器,设置动画时间
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
// 设置动画取值范围和时间曲线
_animation = Tween<Alignment>(
begin: widget.value ? Alignment.centerRight : Alignment.centerLeft,
end: widget.value ? Alignment.centerLeft : Alignment.centerRight,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
));
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (animation, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: _value ? widget.activeColor : widget.inactiveColor,
borderRadius: BorderRadius.circular(widget.height / 2),
),
padding: EdgeInsets.all(_paddingValue),
child: Align(
alignment: _animation.value,
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
_value = !_value;
widget.onChanged?.call(_value);
},
child: Container(
width: _diameter,
height: _diameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.buttonColor,
),
),
),
),
);
},
);
}
}
Hero动画
Hero 指的是在屏幕间转换的 widget。我们可以使用 Flutter’s Hero widget 创建 hero 动画。使 hero 从原页面过渡到新页面。
所以Flutter 中的 Hero widget 实现的动画类型也称为 共享元素过渡 或 共享元素动画。
创建 hero 的步骤
- 定义一个起始 Hero widget,被称为 source hero。也就是要过度的widget,通常是图片。
- 定义一个终点 Hero widget,被称为 destination hero。该 hero 与 source hero 使用一样的 tag 标签,hero 通过 tag 来匹配。 为了获得最佳效果,heroes 应该有几乎完全相同的 widget 树。
- 创建一个含有 destination hero 的页面。目标页面定义了动画结束时应有的 widget 树。
- 通过 Navigator 导航来触发动画。 Navigator 推送并弹出操作触发原页面和目标页面中含有配对标签 heroes 的 hero 动画。
下面我们按照套路实现一个:
第一步,定义一个起始hero;
Hero(
tag: 'flippers',
child: Image.asset(
R.flippers,
),
)
第二部,定义一个终点hero:
Hero(
tag: 'flippers',
child: SizedBox(
width: 100.0,
child: Image.asset(
R.flippers,
),
),
)
第三部,创建个页面装载终点hero:
Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.topLeft,
// Use background color to emphasize that it's a new route.
color: Colors.lightBlueAccent,
child: Hero(
tag: 'flippers',
child: SizedBox(
width: 100.0,
child: Image.asset(
R.flippers,
),
),
),
),
);
第四部,路由导航:
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
...
看下效果:
页面过度动画
hero是2个widget之间的过度,页面的过度需要PageTransitionsBuilder,flutter 给我们实现了4种:
- FadeUpwardsPageTransitionsBuilder — 淡入淡出
- OpenUpwardsPageTransitionsBuilder — 从下往上
- ZoomPageTransitionsBuilder — 从小到大缩放
- CupertinoPageTransitionsBuilder — 苹果左右滑入风格
怎么使用呢?一般我们应用都是一个统一的过度风格,淡然flutter是包含安卓和ios的,所以区分不同的平台对应不同的风格。在全局设置:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
pageTransitionsTheme: PageTransitionsTheme(builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: ZoomPageTransitionsBuilder(),
})),
routes: Routes.routes,
home: MyHomePage(title: '动画'),
);
看下效果:
当然,如果你们的ui特别牛逼,要实现自己的风格,比如旋转并且淡入淡出的过度动画,我们就要自己实现了。
首先实现PageTransitionsBuilder:
class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
const RotationFadeTransitionBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _RotationFadeTransitionBuilder(
routeAnimation: animation, child: child);
}
}
buildTransitions
的返回值是widget,我们创建一个widget:
class _RotationFadeTransitionBuilder extends StatelessWidget {
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) ;
final Widget child;
@override
Widget build(BuildContext context) {
}
}
因为我们需要一个旋转动画和一个淡入淡出,所以我们实现动画:
final Animation<double> _turnsAnimation;
final Animation<double> _opacityAnimation;
https://api.flutter.dev/flutter/animation/Curves-class.html
这是动画对应的curve,我们选择淡入淡出的和旋转的,并通过Animation.drive加到过渡动画上:
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
_opacityAnimation = routeAnimation.drive( CurveTween(curve: Curves.easeIn)),
super(key: key);
实现我们的动画:
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _turnsAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
添加到ThemeData中:
TargetPlatform.android: RotationFadeTransitionBuilder(),
动画全部代码:
import 'package:flutter/material.dart';
class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
const RotationFadeTransitionBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _RotationFadeTransitionBuilder(
routeAnimation: animation, child: child);
}
}
class _RotationFadeTransitionBuilder extends StatelessWidget {
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
_opacityAnimation = routeAnimation.drive( CurveTween(curve: Curves.easeIn)),
super(key: key);
final Animation<double> _turnsAnimation;
final Animation<double> _opacityAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _turnsAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
}
看下效果: