299 lines
12 KiB
Markdown
299 lines
12 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
10 动画使用与状态周期
|
||
1. 什么是动画
|
||
|
||
接下来,我们将进入猜数字项目的最后一个模块:动画的使用。上一节末尾说了,此处使用动画的目的是 增加交互反馈效果 。动画本质上就是不断更新界面展示的内容,玩过翻页动画的感触会深一些:
|
||
|
||
|
||
在每页纸上绘制连续的动作,手快速翻动时,内容快速变化,形成连续运动的动画效果。
|
||
|
||
|
||
|
||
|
||
这里根据翻页动画,先统一给定几个概念描述,方便后续的表述:
|
||
|
||
|
||
动画帧 : 一页纸上的内容。
|
||
动画时长 : 从开始翻看,到结束的时间差。
|
||
帧率 : 动画过程中,每秒钟包含动画帧的个数。
|
||
动画控制动器: 动画进行的动力来源,比如翻页动画中的手。
|
||
|
||
|
||
|
||
|
||
其实对于界面来说也是类似的,屏幕上展示的内容不断变化,给人视觉上的动画效果。对于界面编程来说,动画一般都是改变某些属性值;比如这里是对中间文字的大小进行动画表现:
|
||
|
||
|
||
|
||
|
||
标题
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
我们之前将提示信息的界面封装成 ResultNotice 组件进行展示,现在想要修改面板的展示效果,只要对该组件进行优化即可。可以很快定位到 result_notice.dart, 这也是各司其职的一个好处。下面就来看一下,让文字进行动画变化的流程。
|
||
|
||
|
||
|
||
|
||
|
||
2. 动画控制器的创建
|
||
|
||
要进行动画,首先要找到 驱动力, 也就是翻页的那只手怎么得到。Flutter 框架层对这只手 (Ticker) 进行了封装,给出一个更易用的 AnimationController 类型。想创该类型对象需要两步:
|
||
|
||
|
||
1. 一般在 State 派生类中创建 AnimationController 对象,使用这里将 ResultNotice 改为继承自 StatefulWidget :
|
||
|
||
|
||
class ResultNotice extends StatefulWidget {
|
||
final Color color;
|
||
final String info;
|
||
|
||
const ResultNotice({
|
||
Key? key,
|
||
required this.color,
|
||
required this.info,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<ResultNotice> createState() => _ResultNoticeState();
|
||
}
|
||
|
||
class _ResultNoticeState extends State<ResultNotice>{
|
||
//...
|
||
}
|
||
|
||
|
||
|
||
1. 将状态类通过 with 关键字混入 SingleTickerProviderStateMixin, 让状态类拥有创建 Ticker 的能力。这样在 AnimationController 构造方法中 vsync 入参就可以传入当前状态类。
|
||
对于新手来说,这可能比较难理解,可以先记成固定的流程。不用太纠结,以后有能力时,可以在动画小册中探索更深层的原理。
|
||
|
||
|
||
class _ResultNoticeState extends State<ResultNotice> with SingleTickerProviderStateMixin{
|
||
|
||
late AnimationController controller;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
controller = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(milliseconds: 200),
|
||
);
|
||
}
|
||
|
||
|
||
这样动画控制器对象就在创建完毕了,在创建对象时 duration 入参用于控制动画时长。默认情况下,动画控制器的值会在指定时长内,从 0 匀速变化到 1 。 下面来看一下如何通过动画控制器,来驱动字号大小的变化。
|
||
|
||
|
||
|
||
3. 动画构造器 AnimatedBuilder 的使用
|
||
|
||
动画本身就决定它需要频繁地变化,但很多时候我们只需要局部一小部分进行动画,比如这里只针对于文字。对于这种频繁变化的场景,最好尽可能小地进行重新构建。这里推荐新手使用 AnimatedBuilder 组件,可以非常方便地处理局部组件的动画变化。 由于需要动画的只是文字,所步骤如下:
|
||
|
||
|
||
将 AnimatedBuilder 套在 Text 组件之上。
|
||
将动画控制器作为 animation 入参。
|
||
将需要动画变化的属性值,根据 animation.value 进行计算即可。
|
||
|
||
|
||
|
||
|
||
刚才说过,默认情况下,动画控制器启动之后,它的值会在指定时长内,从 0 匀速变化到 1。所以,这里 fontSize 会从 0 匀速变到 54 。
|
||
|
||
|
||
小思考: 通过简单的数学知识,思考一下如何让 fontSize 从 12 ~ 54 匀速变化。
|
||
|
||
|
||
|
||
|
||
4. 状态类的生命周期回调方法
|
||
|
||
这里介绍一下 State 派生类常用的几个生命周期回调方法;生命周期 顾名思义就是对象从生到死的过程,回调就是生命中特定时机的触发点;回调是 Flutter 框架中触发的,派生类可以通过 覆写 的方式,来感知某个特定时机。
|
||
|
||
比如,我们一般在 initState 回调中处理状态类中成员对象的初始化;如下这里对 controller 对象的初始化。在创建后可以通过 controller.forward 方法,启动动画器,让数值运动:
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
controller = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(milliseconds: 200),
|
||
);
|
||
controller.forward();
|
||
}
|
||
|
||
|
||
|
||
|
||
既然有生,就必然有死。当状态类不再需要时,其中持有的一些资源需要被释放,必然动画控制器。这时可以通过 dispose 回调监听到状态销毁的时机:
|
||
|
||
@override
|
||
void dispose() {
|
||
controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
|
||
|
||
|
||
人除了生和死,就是工作,状态类也是一样。而状态类最重要的工作就是 build 方法构建 Widget 组件,它也是生命周期回调中的一环:
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 略同...
|
||
}
|
||
|
||
|
||
生和死都是只会触发一次,而工作是每天都要进行。所以,框架中对 State 对象的 initState 和 dispose 只会触发一次, build 方法可能触发多次。
|
||
|
||
|
||
|
||
现在一个问题,由于 controller 只会在 initState 触发一次,所以两次校验结果相同,状态类还活着,也只能进行一次动画。状态类如何监听到更新信息呢? 答案也是生命周期回调:
|
||
|
||
|
||
当上级状态触发 setState 时,会触发子级的 didUpdateWidget 生命回调
|
||
|
||
|
||
代码中在点击按钮时,会触发 setState , 所以 _ResultNoticeState 里可以覆写 didUpdateWidget 获得点击的时机,在此触发 controller 的 forward 进行动画。
|
||
|
||
如果 ResultNotice 提供了动画时长的参数,如果外界需要修改动画时长,而外界无法直接访问状态类。就可以通过 didUpdateWidget 来 间接 修改动画控制器的时长。其中 oldWidget 是之前的组件配置信息,另外最新的组件信息是 widget 成员,可以比较两者时长是否不同,对动画控制器进行修改。
|
||
|
||
---->[result_notice.dart]----
|
||
@override
|
||
void didUpdateWidget(covariant ResultNotice oldWidget) {
|
||
controller.forward(from: 0);
|
||
super.didUpdateWidget(oldWidget);
|
||
}
|
||
|
||
|
||
didUpdateWidget 可能对新手来说比较难理解,它提供了外界更新时机的回调,并根据新旧组件配置,来维护 状态类内部数据 。 在 State 的生命之中也可以被调用多次。
|
||
|
||
|
||
当前代码提交位置: result_notice.dart
|
||
|
||
|
||
initState 、build 、didUpdateWidget、dispose 三者是最基础的 State 生命周期回调。除此之外还有几个回调,不过对于新手来说并不重要,以后有能力时可以通过渲染机制小册,从源码的角度去了解它们。
|
||
|
||
|
||
|
||
5. Statless Or Statful
|
||
|
||
对于新手而言,面临的一个非常难的问题就是,我该选择 StatelessWidget 还是 StatefulWidget 。 这其实要取决于你对需求的理解,以及对组件的封装思路:比如这里 ResultNotice,由于想让它进行动画,而动画控制器的控制和维护需要在状态类中处理,所以就选择了 StatefulWidget 。
|
||
|
||
但这并不是绝对的,因为上面选择 StatefulWidget 本质上就是由于 动画控制器 对象。那 ResultNotice 直接摆烂,由构造函数传入动画控制器。这就相当于动画控制器由外界维护,此时 ResultNotice 就可以是 StatelessWidget。
|
||
|
||
|
||
大家可以细品一下 StatefulWidget 变为 StatelessWidget 的过程。先自己思考一下两者的差异,后面我会进行分析。
|
||
|
||
|
||
class ResultNotice extends StatelessWidget {
|
||
final Color color;
|
||
final String info;
|
||
final AnimationController controller;
|
||
|
||
const ResultNotice({
|
||
Key? key,
|
||
required this.color,
|
||
required this.info,
|
||
required this.controller,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Expanded(
|
||
child: Container(
|
||
alignment: Alignment.center,
|
||
color: color,
|
||
child: AnimatedBuilder(
|
||
animation: controller,
|
||
builder: (_, child) => Text(
|
||
info,
|
||
style: TextStyle(
|
||
fontSize: 54 * (controller.value),
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
当前代码提交位置: result_notice.dart
|
||
|
||
|
||
|
||
|
||
俗话说,冤有头,债有主 。只要还想进行动画,控制器就需要有一个状态类做接盘侠(维护者)。由于 ResultNotice 已经甩锅了,那这里就需要上层状态 _GuessPageState 来维护:
|
||
|
||
|
||
|
||
这时,在构建 ResultNotice 时,传入控制器即可:
|
||
|
||
Column(
|
||
children: [
|
||
if(_isBig!)
|
||
ResultNotice(color:Colors.redAccent,info:'大了',controller: controller,),
|
||
Spacer(),
|
||
if(!_isBig!)
|
||
ResultNotice(color:Colors.blueAccent,info:'小了',controller: controller,),
|
||
],
|
||
),
|
||
|
||
|
||
最后在点击时触发动画器:
|
||
|
||
|
||
|
||
|
||
|
||
现在就 ResultNotice 来分析一下 StatlessWidget 和 StatefulWidget 在使用上的差异性。
|
||
|
||
|
||
ResultNotice 为 StatefulWidget 时,外界使用者无需和 AnimationController 就能进行动画。也就是说将动画控制器的逻辑封装到了内部,拿来即用,用起来简洁方便。
|
||
ResultNotice 为 StatlessWidget 时,外界使用者需要主动维护 AnimationController 对象,使用门槛较高。另外,由于使用者主动掌握控制器,可以更灵活地操作。
|
||
之前是静态的界面,现在想要进行动画,对于功能拓展来说,使用 StatefulWidget 来独立维护状态的变化。可以在不修改之前其他代码的前提下,完成需求。
|
||
|
||
|
||
好用 和 灵活 是一组矛盾,封装度越高,使用者操心的事就越少,用起来就越好用。但同时想要修改封装体内部的细节就越麻烦,灵活性就越差。所以,没有什么真正的好与坏,只有场景的适合于不适合。在面临选择时,要想一下:
|
||
|
||
|
||
你是只想看电视的用户,还是以后电视开膛破肚的维修也要自己做。
|
||
|
||
|
||
Flutter 中内置的很多 StatefulWidget 组件,我们就是使用者。比如点击按钮有水波纹的变化、点击 Switch 有滑动效果等,这些内部的状态变化逻辑是不用我们操心的。作为使用者可以非常轻松地完成复杂的交互效果,这就是封装的优势。但同时,如果需求的表现有一点不符合,改框架源码将会非常复杂,门槛也很高,这就是封装的劣势。对于选取 StatfulWidget 我的标准是:
|
||
|
||
|
||
当前组件展示区域,在交互中需要改变内容;且外界无需在意内部状态数据。
|
||
|
||
|
||
对于 ResultNotice 来说, StatlessWidget 或 StatefulWidget 差别并不是非常大,只是 AnimationController 交给谁维护的问题。每种方式都有好处,也有坏处,但都可以实现需求。所以结合场景,选取你觉得更好的即可。对于新手而言,能完成需求是第一要务,至于如何更优雅,你可以在以后的路中慢慢揣摩。
|
||
|
||
|
||
|
||
6.本章小结
|
||
|
||
本章主要介绍了 Flutter 中使用动画的方式和步骤,并简单了解了一下状态类的生命周期回调方法。最后分析了一个对于新手而言比较重要的话题 StatlessWidget 和 StatefulWidget 的差异性。
|
||
|
||
到这里,我们的第一个猜数字小案例的全部功能就实现完毕了。从中可以了解很多 Flutter 的基础知识,在下一篇中将对猜数字的小案例进行一个总结,看看现在我们已经用到了哪些知识,以及当前代码还有哪些优化的空间。
|
||
|
||
|
||
|
||
|