动画
UI 界面设计合理的动画,可以让用户觉得更加流畅、直观,可以极大提高和改善用户体验
实现原理
动画就是动起来的画面
视觉暂留:画面经视神经传入大脑后,不会立即消失(会存留一段时间)
帧(Frame):单个的画面,在学术上叫帧
每秒钟展示的帧数简称 fps (Frame per Second)
动画分类
补间(Tween)动画
在补间动画中,我们定义开始点和结束点、时间线以及定义转换时间和速度曲线。然后由系统计算,从开始点运动到结束点。
从而形成动画效果。例如:透明度从 0 到 1,颜色值从 0 到 255
拟物动画
拟物动画是对真实世界的行为进行建模,使动画效果类似于现实中的物理效果。例如:弹簧、阻尼、重力、抛物线等
动画 - Animation
Animation,是 Flutter 动画库中的一个核心类。它包含动画的值和状态两个属性,定义了动画的一系列监听函数。
监听值:
addListener
removeListener
监听状态:
addStatusListener
removeStatusListener
动画状态
AnimationStatus.dismissed -- 动画初始状态
AnimationStatus.completed -- 动画结束状态
AnimationStatus.forward -- 动画处在从开始到结束的运行状态
AnimationStatus.reverse -- 动画处在从结束到开始的运行状态
动画 - AnimationController
AnimationController(动画控制器)
在指定时间内,将组件属性值由初始值演变到终止值。从而形成动画效果
AnimationController 参数
duration(动画的执行时间)
reverseDuration(动画反向执行时间)
lowerBound = 0.0 (动画最小值)
upperBound = 1.0(动画最大值)
value (动画初始值,默认是 lowerBound)
vsync (TickerProvider 类型的对象,用来创建 Ticker 对象)
当创建一个 AnimationController 时,需要传递一个 vsync 参数
vsync 的作用是:防止屏幕外动画(动画页面切换到后台时)消耗不必要的资源
通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 stateful 对象作为 vsync 的值
AnimationController 具有控制动画的方法:
.forward() 可以正向执行动画
.reverse() 可以反向执行动画
.dispose() 用来释放动画资源(在不使用时需要调用该方法,否则会造成资源泄漏)
.stop() 用来停止动画运行
动画 - Tween
简介
AnimationController 动画生成值的默认区间是 0.0 到 1.0,如果希望使用不同的区间,或不同的数据类型,需要使用 Tween(补间动画)
Tween 的唯一职责就是定义从 输入范围 到 输出范围的映射,例如:颜色区间是 0 到 255
Tween
Tween<double>(begin:起始值,end:终止值);
ColorTween(begin: Colors.white, end: Colors.black);
动画 - CurvedAnimation
简介
动画执行的速度有多种(匀速、先快后慢或先慢后快)这里的速度称为动画曲线
CurvedAnimation 的目的是为 AnimationController 添加动画曲线
组件
CurvedAnimation(parent: controller, curve: Curves.easeIn)
parent(动画控制器对象)
curve(正向执行的动画曲线)
reverseCurve(反向执行的动画曲线)
Curves
动画曲线:https://api.flutter-io.cn/flutter/animation/Curves-class.html
动画 - 步骤
(1)、创建动画控制器
controller = AnimationController(duration, vsync)
(2)、创建动画
动画曲线(CurvedAnimation)
补间动画(Tween)
(3)、监听动画
addListener(); // 监听动画生成值
addStatusListener(); // 监听动画状态
(4)、执行动画
controller.forward(); // 正向执行
controller.reverse(); // 反向执行
import 'package:flutter/material.dart'; class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Animation'), ), body: const AnimationDemo(), ); } } class AnimationDemo extends StatefulWidget { const AnimationDemo({Key? key}) : super(key: key); @override State<AnimationDemo> createState() => _AnimationDemoState(); } class _AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin { late AnimationController controller; late Animation animation; @override void initState() { // TODO: implement initState super.initState(); // 1、创建 AnimationController controller = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); // 2-1、声明动画曲线 animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn); // 2-2、设置动画值的范围 animation = Tween(begin: 50.0, end: 200.0).animate(controller); // 3、监听动画 animation.addListener(() { setState(() {}); }); // 4、执行动画 controller.forward(); } @override Widget build(BuildContext context) { return Center( child: Column( children: [ ElevatedButton( onPressed: () { controller.forward(); }, child: const Text('放大'), ), ElevatedButton( onPressed: () { controller.reverse(); }, child: const Text('缩小'), ), ElevatedButton( onPressed: () { animation.addStatusListener((status) { print('status: $status'); if (status == AnimationStatus.completed) { // 反向执行动画 controller.reverse(); } else if (status == AnimationStatus.dismissed) { // 正向执行动画 controller.forward(); } }); controller.forward(); }, child: const Text('重复'), ), ElevatedButton( onPressed: () { controller.stop(); }, child: const Text('停止'), ), Icon( Icons.favorite, color: Colors.red, size: animation.value, ), Opacity( opacity: controller.value, child: const Text('Hello Flutter'), ), ], ), ); } @override void dispose() { // TODO: implement dispose super.dispose(); controller.dispose(); } }
交织动画
What?
交织动画是由多个单一动画叠加而成的复杂动画
例如:组件变化可能涉及高度、宽度、颜色、透明度、位置等等
需要给每个动画设置时间间隔(Interval)
Transform(对组件进行矩阵变换)
平移:Transform.translate()
旋转:Transform.rotate()
缩放:Transform.scale()
import 'package:flutter/material.dart'; import 'dart:math'; class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Stagger Animation'), leading: const Icon(Icons.menu), actions: const [Icon(Icons.settings)], elevation: 0.0, centerTitle: true, ), body: const AnimationDemo(), ); } } class AnimationDemo extends StatefulWidget { const AnimationDemo({Key? key}) : super(key: key); @override State<AnimationDemo> createState() => _AnimationDemoState(); } class _AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin { late AnimationController controller; late Animation<double> animation; late Animation sizeAnimation; late Animation colorAnimation; late Animation rotationAnimation; @override void initState() { // TODO: implement initState super.initState(); // 1、创建 AnimationController controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), ); // 2、创建动画 animation = CurvedAnimation( parent: controller, curve: const Interval(0.0, 0.5), )..addListener(() { setState(() {}); }); // 3、让动画反复执行 animation.addStatusListener((status) { if (status == AnimationStatus.completed) { // 反向执行动画 controller.reverse(); } else if (status == AnimationStatus.dismissed) { // 正向执行动画 controller.forward(); } }); // 4、设置其它动画 sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(animation); colorAnimation = ColorTween(begin: Colors.yellow, end: Colors.red).animate( CurvedAnimation( parent: controller, curve: const Interval(0.5, 0.8, curve: Curves.bounceIn), )..addListener(() { setState(() {}); }), ); rotationAnimation = Tween(begin: 0.0, end: 2 * pi).animate( CurvedAnimation( parent: controller, curve: const Interval(0.8, 1.0, curve: Curves.easeIn), ), ); } @override Widget build(BuildContext context) { return Center( child: Column( children: [ ElevatedButton( child: const Text('重复'), onPressed: () { animation.addStatusListener((status) { print('status, $status'); if (status == AnimationStatus.completed) { // 反向执行动画 controller.reverse(); } else if (status == AnimationStatus.dismissed) { // 正向执行动画 controller.forward(); } }); controller.forward(); }, ), ElevatedButton( onPressed: () { controller.stop(); }, child: const Text('停止'), ), Icon( Icons.favorite, color: Colors.red, size: sizeAnimation.value, ), Opacity( opacity: controller.value, child: Transform.rotate( angle: rotationAnimation.value, child: Container( width: sizeAnimation.value, height: sizeAnimation.value, color: colorAnimation.value, ), ), ), ], ), ); } @override void dispose() { // TODO: implement dispose super.dispose(); controller.dispose(); } }
Hero 动画
Hero 动画用来实现跨页面的动画效果
在不同页面中,声明一个共享组件(Hero)
由于共享组件在不同页面中的位置、外观等不同,路由切换时,形成动画效果
如何实现
在页面A中定义起始 Hero 组件(source hero),声明 tag
在页面B中定义 目标 Hero 组件(destination hero),绑定相同的 tag
页面跳转时,通过 Navigator,传递 tag
Hero 组件
tag(路由切换时,共享组件的标记)
child(声明子组件)
// image_detail.dart import 'package:flutter/material.dart'; class ImageDetail extends StatelessWidget { final String imageUrl; const ImageDetail({ Key? key, this.imageUrl = '', }) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: GestureDetector( onTap: () { // 返回 Navigator.pop(context); }, child: Hero( tag: imageUrl, child: Image.network( imageUrl, width: double.infinity, fit: BoxFit.cover, ), ), ), ), ); } }
import 'package:flutter/material.dart'; import 'image_detail.dart'; class Home extends StatelessWidget { const Home({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Hero Animation'), ), body: const HeroAnimation(), ); } } class HeroAnimation extends StatefulWidget { const HeroAnimation({Key? key}) : super(key: key); @override State<HeroAnimation> createState() => _HeroAnimationState(); } class _HeroAnimationState extends State<HeroAnimation> { @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(10), child: GridView.extent( maxCrossAxisExtent: 200.0, // 子组件最大宽度 mainAxisSpacing: 20, children: List.generate(20, (index) { String imageUrl = 'https://picsum.photos/id/$index/300/400'; return GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (BuildContext ctx) { return ImageDetail(imageUrl: imageUrl); })); }, child: Hero( child: Image.network(imageUrl), tag: imageUrl, ), ); }), ), ); } }