动画动作分析基础知识

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动画动作分析基础知识相关的知识,希望对你有一定的参考价值。

参考技术A

动画动作分析基础知识

  知识是符合文明方向的,人类对物质世界以及精神世界探索的结果总和。知识,也没有一个统一而明确的界定。以下是我为大家整理的动画动作分析基础知识,欢迎阅读,希望大家能够喜欢。

  一、动画时间的技巧

  1、时间与帧数

  对动画时间的基本考虑是放映速度:电影和电视的放映速度是24帧/秒,而动画片一般有12帧就可以了,然后录制或拍摄时进行双格处理.如果绘制动作较快的动画最好进行单格处理,即每秒要绘制24个画面.对於快速奔跑的动作,一般采用8帧单格画面.对於物体发生震动用单格处理两端的动作就可以了。

  2、动画的间格距离表现

  物体的静止到移动到静止都有类似的规律:静止开始时速度慢、运动中的速度快、运动停止时的速度慢.表现在帧数上则是:从静止到运动帧数逐渐减少,从运动到静止帧数逐渐增加,中间运动过程的速度最快,帧数也最少。

  3、循环动作的时间

  在动画中经常会有循环动作,但不同的情况需要的帧数也是不同的.如:快速飘扬的旗需要6帧画面循环;又如火焰的循环,大火的动作循环从底部烧到顶部可能需要几秒,而小火的循环只需要几帧;下雨的循环动作最好设置两层,前层雨水穿过屏幕,一般需6帧画面,后层雨水穿过屏幕的时间慢於前层,循环的帧数也相应多於前层;下雪的动画则至少需要有3种大小不同的雪花,循序的时间约需要2秒;一个急速跑步动作需4帧画面,快跑动作需8帧画面,慢跑动作则需12帧,超过16 帧,画面就失去冲刺感觉;大象需要1~1.5秒完成一个完整的步子;小动物如猫的一个动作只需0.5 秒或更少;鹰的翅膀一个循环需要8帧;小麻雀的翅膀循环动作有2帧画面就可以了。

  第四课:动画动作的预感处理与夸张

  一、动作预感处理

  在动画设计中,为了吸引观众的视力,在处理一个快速动作即将发生之前所出现的准备动作,叫预感设计,如果缺少这个步骤吸引观众视力,直接进入快速动作,就会使观众看不到或没有注意到其中的关键环节,以至於失去故事情节的线索,快速动作也会失去作用.如表现一个人冲出画面的情景,先制作出冲的准备动作,准备动作要处理的慢些,而冲出画面的动作只要两帧画面即可,余下的可用10多帧画面来处理跑出画面的路线和慢慢散开的灰尘。

  二、动画的夸张

  大多数动画都是以漫画形式进行创作的,不仅在形象设计上需要夸张主要特征,在动作设计上也要进行大胆的夸张.动画片大都是给小朋友观看的,因而更需要色彩鲜艳、形象和动作的夸张.如一个球从空中落地,真实情况下的形态是不会有明显变化的,在动画形象的处理上,要将坠落的球描绘成较长的椭圆形,当球落地时则把它表现为扁平的球。

  第五课:动画中人物走路动作

  一、人的走路动作的基本规律

  人在走路中的基本规律是:左右两脚交替向前,为了求得平衡,当左脚向前迈步时左手向后摆动,右脚向前迈步时右手向后摆动.在走的过程中,头的高低形成波浪式运动,当脚迈开时头的位置略低,当一脚直立另一脚提起将要迈出时,头的位置略高。

  二、人走路的速度节奏变化

  人走路的速度节奏变化也会产生不同的效果.如描写较轻的走路动作是"两头慢中间快",即当脚离地或落地时速度慢,中间过程的速度要快;描写步伐沉重的效果则是"两头快中间慢" ,即当脚离地或落地时速度快,中间过程速度慢。

  人的走路动作一般来说是1秒产生一个完整步,每一个画面以两帧处理,一个完整步需12(帧)个画面,总的帧数是24帧画面,这种处理方法通常被称为"一拍二"。

  第六课:动画中人物奔跑动作

  一、人的奔跑动作的基本规律

  人在奔跑中的基本规律是:身体中心前倾,手臂成屈曲状,两手自然握拳,双脚的跨步动作幅度较大,头的高低变化也比走路动作大.在处理急速奔跑的动画时,双脚离地面的动作可以处理为1-2帧画面,以增加速度感。

  人跑步动作一般来说是1秒产生两个完整步,制作时以"一拍一"方式,即每张画面只出现一次,描写1秒中的跑步动作要绘制成半秒完成一个完整步,另外的半秒画面重复前面的画面即可。

  第七课:动画中人物面部表情

  人的面部表情

  人的面部表情是非常丰富的,在动画中表现人物表情时应注意表情的夸张,以此来增加动画人物的趣味性和观赏性。

  人的面部表情和说话时的口形变化是动画设计中经常出现的画面,必须研究并掌握其规律.人物的表情要根据剧情的需要进行夸张处理.口形的变化是与说话的内容和动作相联系的.如果忽视人物说话时所需的时间,将会给配音同步带来麻烦。

  第八课:动画中动物动作规律(1)

  一、兽类

  1、跑步:兽类动物跑步的基本规律是:跑的越快四条腿的交替分合越不明显,身体的伸展和收缩差距越大,在快速奔跑时四条腿全离地.一般快跑动作一个循环需要11-13张画面(一拍一),跳跃式奔跑动作一个循环需要5-9张画面(一拍一)。

  2、走路:兽类动物的走路基本规律是,走路时四脚两分两合,左右交替,前脚向后时关节向后弯,后脚向后时关节向前弯.带爪的动物运动时关节运动不明显,动作柔软,有蹄的动物运动时关节运动较明显,动作硬直。

  在速度处理上,兽类动物走路的一般速度是:马的慢走速度15张画面一个循环(一拍二),狗的慢走动作一个循环需要13张画面(一拍二)。

  第九课:动画中动物动作规律(2)

  二、禽类

  1、家禽:

  鸡的走路动作基本规律是身体略向左右摆动,为了取得平衡,头和脚配合动作,当后脚抬起时头向后收,脚抬到中间最高点时,头收缩到最里面,当脚向前伸展落地时头伸展的最长。

  鸭子在走路时左右摇摆幅度较大,游泳时双脚前后交替划动,动作轻柔,身体的尾巴略向左右摆动。

  2、飞禽:

  飞禽分为阔翼类(如鹰、鹤、大雁、海鸥等)和雀类(如麻雀、燕子等).阔翼类飞禽翅膀一般比较大,以飞翔为主,飞行时翅膀上下扇动变化较多,动作柔软优美缓慢,飞行时经常有滑翔动作.雀类飞禽一般身体小而圆,翅膀不大,动作灵活,飞行速度较快,动作快而急促,飞行时扇动翅膀的频率较高。

  第十课:动画中动物动作规律(3)

  三、鱼类

  鱼类动作大概可分为3种,大鱼动作慢而稳,游动时曲线弧度较大;小鱼动作快而敏捷,节奏短促,时常停顿或突然穿游;长尾鱼如金鱼,尾部的曲线运动变化较多,动作优美,柔软缓慢。

  四、昆虫类

  自然界的昆虫种类繁多,以动画中常用的昆虫角色为例说明昆虫的动作规律。

  蝴蝶在飞行时由於翅大身轻会随风飞舞,绘制蝴蝶动画时只用画两张画,即一张翅膀向上,一张翅膀向下,只要把翅膀的飞翔路径指定好,在动画制作方面极为简单。

  蜜蜂和苍蝇是身体小翅膀小的昆虫,在飞翔的时候翅膀的震动频率极高,在设计的时候先设置运动路径,翅膀的运动只画向上和向下两张画面即可。

  蜻蜓的飞行动作平稳,翅膀的动作频率很高,可在一张画上绘制几个翅膀的虚影即可。

  五、其他

  其他如蛇、龟、青蛙等各种动物的动作规律都各有不同,必须认真观察每个动物的不同习性和运动规律,在动画中的动物往往都带有拟人色彩,在动作上除了保留动物本身的动作特点外,还要有人的一些动作特征。

  第十一课:自然形态的运动规律——风

  一、运动线表现

  风本来是无形的,要表现他只有靠别的物体的漂移、运动,如被风吹起的落叶、纸屑等,在设计时一般先设计好物体的运动线,确定被风吹起的物体动作的转折点,然后逐张绘制中间画。

  二、曲线运动表现

  曲线运动表现最常见的有飘带、胸前的红领巾、飘扬的红旗等一端固定的柔软物体。

  三、流线表现

  动画影片中的龙卷风、较大的风吹起的纸屑、沙土等物,以及风冲击较大物体时的情景,一般可以采取流线的表现方式。

  四、拟人化表现

  根据动画影片剧情和艺术风格的需要,有时会把风直接夸张成拟人化艺术形象,在设计时既考虑风的运动规律,又相对灵活不受其影响,充分发挥夸张和想象力。

  第十二课:自然形态的运动规律——雨和雪

  一、雨的表现

  由于人的眼睛的视觉残留原理,雨的降落给人的视觉感觉就成了直线形.为了丰富下雨时的真实效果,绘制下雨动画时一般要画三层画面.第一层画面表现大雨点;第二层画面表现较粗的直线;第三层画面表现细而密的直线;将三个画面重合在一起时,画面将变得很丰富.这三层的下雨速度也应有所区别,大雨点下落,从屏幕上出现到消失的画面需要6-8帧;粗线状下落需要8-10帧;细线状下落需要12-16帧。

  二、雪的表现

  雪的下落速度比较缓慢,为了丰富雪的动画画面,也需要绘制三层画面组合,第一层画面绘制大雪花;第二层画面绘制中等大小的雪花;第三层画面绘制小雪花.雪花的下落路径是曲线状态,一般先设置好下雪路径之后再绘制雪花形状。

  第十三课:自然形态的运动规律——雷电

  一、雷电

  雷电有两种表现方法:一种是不出现闪电光带的亮光表示法,另一种是带光带的\'表示法.在电脑动画影片中亮光表示雷电的方法是在该画面上罩上一层透明的白色光.一次闪电大概需要5-6帧画面,也可一次闪电后间隔6-8帧后紧接第二个闪电。

  带闪电带的表示方法一般由7-8帧画面完成从开始到结尾的全过程,闪电的形状千变万化,可以绘制成树枝状,可以绘制成图案状。

  第十四课:自然形态的运动规律——水

  在动画设计中水的变化比较复杂,归纳起来大概有聚合、分离、推进、"S"形变化、曲线变化、扩散变化及波浪变化等几种。

  一、水圈:一件物体坠落到水中就会产生水圈纹,水圈一般从形成到消失需要8帧画面即可。

  二、微波:一件物体在水中前进,如小船行驶,就会在水面上产生水纹微波,在表现这种微波时需要注意水纹应逐渐向物体外侧扩散;水纹逐渐向物体相反方向拉长、分离、消失;水纹动作速度不宜太快,物体尾部呈现曲线形波纹,并逐渐向后消失。

  三、水流:江河、小溪等流水形态,一般水流向前推进一步需要7张画面,在动画影片中每帧画面拍二格。

  四、水花:一盆水或较重的物体投入水中时水面溅起的水花,一般需5-10帧画面。

  五、水浪:江河湖海中水浪的运动在风力或其他力作用下会产生许多变化,一般从水浪的形成到消失一个循环有8帧画面即可。

  第十五课:自然形态的运动规律——火

  一、火

  火是动画影片中常出现的自然形态,火的运动也是不规则运动,受风的影响其形态变化显著,基本动态大概有:扩张、收缩、摇晃、上升、下降、分离和消失七种.处理小火的动态时要灵活而快捷,处理大火的动作要慢一些,为丰富大火的画面效果,必须作成多层次才能产生出立体效果。

  第十六课:自然形态的运动规律——爆炸

  一、爆炸

  爆炸是突发性的状态,动作猛烈,速度极快.动画影片中表现爆炸场面主要从三个方面进行描绘。

  1、强烈的散光。

  2、被炸飞的各种物体。

  3、爆炸时产生的烟雾。

  强烈的散光效果一般需求8-12帧画面,表现方法归纳为三种.其一是用深浅差别很大的两种色彩的突变来表现强烈的散光效果;其二是放射形散光出现后从中心撕裂也可表现出散光的强烈效果;其三是用扇形扩散的方法表现散光。

  动画行业的人才无非就是两类,动画技术人才和动画艺术人才。这两种人才并不是绝对分离的,搞技术的人要懂美术,搞艺术创作的人也要懂技术,技术与艺术相结合的高级复合型人才是我国动画高等教育的培养目标,也将是市场需求最大的人才。

  【拓展】动作脚本交互式动画设计

  从实质层面上看,动作脚本也即Flash提供给我们的一些运算符与对象。Flash中脚本命令被简单地称之为工作脚本,英文表示为ActionScript。Flash的应用,为我们营造了更多逼真的动画效果,让交互性变成可能,当点击按钮,便可实施人机交互,这便需要使用到动作脚本。代码控制在动作脚本中,组成了Flash交互性不可缺少的一部分。将动作脚本方法应用在Flash,简单地划分为以下两种形式,一种形式是,将脚本编写到对象上,如,影片剪辑元件,另一种种形式是,将脚本编写到时间轴上的关键帧。

  1Flash中不易分清的概念

  FLASH动画的各个对象的位置关系是按照一定的层状结构展现各对象间的位置关系。其根为场景。各场景是相对独立的动画,FLASH设置各场景播放顺序,逐个衔接各动画场景,所以我们看到的动画是持续的,在编辑过程中,各场景实例是不能够应用在其余场景中的,最好应用在相同场景编辑中。对场景播放顺序,设置时,借助窗口一面板一场景。对具体的一个场景而言,和其余场景结构是相同的。均涵盖了一个或多个图层。

  2动画的设计与实现

  2.1动画实现的目标

  动画多是为了让文字紧随鼠标来变动,以鼠标作为圆心,进行圆周运动,此外,文字颜色表现出色彩变动。

  2.2动画原理的分析

  ①窗口鼠标、文字、舞台坐标间的位置关系。鼠标移动同时,文字也要移动,同时围绕鼠标做圆周运动,文字坐标值指的是圆周上的某点。鼠标坐标值紧随鼠标移动而改变,同时,文字鼠标值也相应发生改变。默认的坐标原点O:(0,0)位在窗口左上角,圆心O:(h,k)代表鼠标在窗口舞台上的坐标值,在坐标系中,也就是将圆心O不再是,而被移到了(h,k)。按照圆心O:(h,k),再次将直角坐标系构建起来,P点表示的圆心为点(h,k),半径为r,为圆上一点坐标,是文字坐标位置所表示的区域。鼠标的坐标值(h,k)、P点的坐标值(x,y)二者之间的关系可用下述公式来表示:。事实上,Flash里对三角函数里的角做出了规定,其单位要为“弧度”,1度与π/180弧度是相等的,Flash中的P点的坐标可用下述公式来表示:arkyarhx180/.(sin()),180/.(cos()。在本次获得的动画效果中,需要的文字对象数目为N个,以鼠标为中心做圆周运动,圆周上面的平均分配的P点坐标数目需要N个,各P点坐标的表示则为:180/())./360sin((.)),180/()./360cos(()xryiNhriNkfalse[1]。圆周上的i代表的是圆周上的第几个文字。应当引起主要的一点是,P点坐标指的是文本域注册点的坐标值,在实践当中,要将文本域中心点移至P点位置。②关键的处理函数。Math.random()函数能够形成随意的小数,处在0~1间,Math.round()函数以四舍五入的形式获得相似整数,两者整合起来,应用在文字随机颜色创建中,让文字颜色不断表现出色彩变换;对addEventListener事件侦听函数,当有数值出现改变,其按照变化后的数值对其余变量数值做出新的计算。

  2.3动画实现的流程

  第一步,新建Flash的文档,把舞台大小设置为550px×400px;第二步,新建影片剪辑元件,将其名字命名为apple,把眼球图形绘制到元件编辑窗口中,设定其半径为50,采用对齐工具,在眼球圆心中放入注册点,和途中的点B(a,b)保持对应关系;第三步,绘制左圆形眼眶,左眼眶中心(150,150),绘制右圆形眼眶,右眼眶中心(400,150),半径R=100,全部放置在场景编辑窗口中,和点A(m,n)保持对应关系;第四步,将2个apple元件实例于库面板中拖出来,一个命名为left_apple,表示左眼球,另一个命名为right_apple,表示右眼球;第五步,根据以动画原理分析为根据,同时结合获得的计算公式,将相应代码添加至图层1的第一帧中;第六步,装饰动画,把眉毛添加到眼睛上,同时将含微笑的嘴巴放到眼睛的下部,显得更逼真;第七步,经过测试,并得到影片。

  3结论

  本次研究使用了Flash的动作脚本,让交互性动画生动地呈现出来,对脚本中的部分参数,做出设置,生成各种动画效果,如,变量d能够对文字的转动频率实施调控,计算公式d+=0.05中,0.05数值发生改变后,产生的文字转动频率是不同的,值变小,文字转动变慢,值变大时,文字转动变快,+/-号在公式中可以对文字的转动方向进行调控,变成顺时针/逆时针;文字字号大小设置公式公式format.在文字转动过程中,同时将文字调下或调大。像上述设置,结合实际需求,做出相应调整。切实掌握动作脚本,便能营造出各类交互性动画效果。

;

Flutter 基础 | 动画框架分析及其中的设计模式

作者:唐子玄

在阅读 Flutter 动画源码时收获颇多,深深地被它的设计框架及代码实现方式所折服~~

若不关心源码,想直接上手动画实战代码可以跳到动画实例解析

动画源码解析

动画值序列 Tween

动画是为了缓解值的“跳变”,跳变体验不好。

比如弹窗通常是由小变大(scale),由浅变深(alpha),为了让弹窗不那么突兀,就得生成 scale 和 alpha 的值序列,让弹窗一点一点变大,一点一点显现出来。

在 Flutter 中生成动画值序列的工作是由Tween完成的。

Tween 继承自Animatable:

class Tween<T extends Object?> extends Animatable<T> 

// 可动画的对象
abstract class Animatable<T> 
  // 根据动画进度 t 生成对应动画值 T(动画进度为[0,1])
  T transform(double t);

Animatable 是一个抽象类,其中最重要的方法是T transform(double t),它定义了一个生成动画值的算法,即根据给定动画进度生成单个动画值。

所以Animatable是一个将动画进度转换为动画值的对象。

Tween 重写了 transform():

class Tween<T extends Object?> extends Animatable<T> 
  // 动画值序列起点
  T? begin;
  // 动画值序列终点
  T? end;

  @override
  T transform(double t) 
    if (t == 0.0)
      return begin as T; // 若动画进度为0,直接返回起点值
    if (t == 1.0)
      return end as T; // 若动画进度为1,直接返回终点值
    return lerp(t); // 否则,返回lerp(t)
  

Tween 在 Animatable 基础上新增了2个成员变量表示动画值序列的起点和终点,对应动画进度的 0 和 1。除了这两个极值之外的其他动画值通过lerp()方法生成:

class Tween<T extends Object?> extends Animatable<T> 
  @protected
  T lerp(double t) 
    ...
    return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
  

lerp 是“线性插值”的意思。在直角坐标系中,动画值是动画进度 t 的函数,而且这是一段斜线,它的斜率和离原点的偏移量由 begin 和 end 决定。

所以Tween是一个将动画进度转换为给定区间动画值序列的对象,称之为补间

大部分 Tween 的子类通过重写lerp()方法实现不同的线性插值,比如ReverseTween:

class ReverseTween<T extends Object?> extends Tween<T> 
  ReverseTween(this.parent)
    : assert(parent != null),
      super(begin: parent.end, end: parent.begin);

  final Tween<T> parent;

  @override
  T lerp(double t) => parent.lerp(1.0 - t);

ReverseTween 表示反向补间,所以它的 lerp() 委托给了另一个补间对象并传入反向的动画进度1.0-t

再比如ColorTween

class ColorTween extends Tween<Color?> 
  ColorTween( Color? begin, Color? end ) : super(begin: begin, end: end);

  @override
  Color? lerp(double t) => Color.lerp(begin, end, t);

ColorTween 表示颜色补间,它把插值算法委托给了Color.lerp(),该方法内部分别对A,R,G,B实现了插值。

这就是典型的模板方法模式,即在父类实现算法框架,并将算法的某些步骤抽象为方法,子类通过重写该方法来替换掉算法的某个步骤。

模板方法的目的是复用算法框架,在当前场景的算法框架即是将动画进度转换成动画值的算法:

class Tween<T extends Object?> extends Animatable<T> 
  // transform 这个算法框架在父类 Tween 得以固定,
  // 子类通过重写 lerp() 实现各种不同的转换
  @override
  T transform(double t) 
    if (t == 0.0)
      return begin as T; 
    if (t == 1.0)
      return end as T; 
    return lerp(t); 
  

关于模板方法模式更详细的介绍可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

并不是所有的插值都是线性的,有些插值是一条曲线,CurveTween就是用来表达曲线插值的补间:

class CurveTween extends Animatable<double> 
  CurveTween( required this.curve )
    : assert(curve != null);
  // 曲线
  Curve curve;

  @override
  double transform(double t) 
    if (t == 0.0 || t == 1.0) 
      assert(curve.transform(t).round() == t);
      return t;
    
    return curve.transform(t); // 由具体的曲线实现插值
  

CurveTween 重写了transform()并把它委托给对应的曲线。

动画进度 AnimationController

仅有动画值序列还不能满足做动画的需求,就好比即使知道完整的音符序列也不能演奏出美妙的曲子,因为没有节奏信息,即不知道每个音符应该在哪个时间点被激活。

在 Flutter 中动画节奏信息由AnimationController承载。

class AnimationController extends Animation<double> 

AnimationController 继承自 Animation。而 Animation 继承自Listenable

// 动画对象
abstract class Animation<T> extends Listenable implements ValueListenable<T> 

// 可监听对象
abstract class Listenable 
  // 添加监听器
  void addListener(VoidCallback listener);
  // 移除监听器
  void removeListener(VoidCallback listener);


// 可监听的值
abstract class ValueListenable<T> extends Listenable 
  // 值
  T get value;

Animation是一个可监听的值,用泛型声明,表示值的类型任意。而且它是抽象的,获取动画值会推迟到子类 AnimationController 中定义。

class AnimationController extends Animation<double> 
  @override
  double get value => _value;
  // 动画进度
  late double _value;
  // 时钟
  Ticker? _ticker;

AnimationController 把获取动画值委托给了一个私有成员_value,它的语义是动画进度。

AnimationController 中有另一个私有成员叫_ticker,它是一个时钟:

class Ticker 
  // 滴答回调
  final TickerCallback _onTick;

  // 启动时钟
  TickerFuture start() 
    ...
    scheduleTick();
  

  // 安排一次滴答
  void scheduleTick( bool rescheduling = false ) 
    ..
    // 注册下一帧的绘制并执行_tick
    _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick,   rescheduling: rescheduling);
  

  void _tick(Duration timeStamp) 
    // 回调滴答给上层
    _onTick(timeStamp - _startTime!);
    //
    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  

当时钟被启动后,它会注册每一帧的绘制,并通过回调传递给上层。当前 Ticker 的上层即是 AnimationController:

class AnimationController extends Animation<double> 

  AnimationController(
    ..
    required TickerProvider vsync,
  ) 
    // 构建时钟
    _ticker = vsync.createTicker(_tick);
  

  void _tick(Duration elapsed) 
    ..
    // 计算逝去的时间
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    // 根据时间生成新的动画值
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    // 通知动画监听者
    notifyListeners();
  

AnimationController 在构造方法中新建时钟实例,动画开始就启动时钟,在绘制的每一帧根据逝去的时间生成当前动画进度,最后通知动画进度监听者。(通常是Widget)

AnimationController 承载动画节奏信息。其内部有一个时钟,用于持续地监听下一帧的绘制,并通知上层,AnimationController 根据绘制的节奏再结合流逝的时间按照一定的算法计算出当前的动画进度并通知观察者(Widget)。

动画进度 -> 动画值序列

AnimationController 生成动画进度,而 Tween 根据动画进度生成动画值序列。它俩是如何结合在一起的?

答案是Animatable.animate()

abstract class Animatable<T> 
  // 根据动画进度 t 生成对应动画值 T(动画进度为[0,1])
  T transform(double t);
  // transform() 的包装方法,由 animation 提供动画进度
  T evaluate(Animation<double> animation) => transform(animation.value);
  // 将一个 Animation 转换成另一个 Animation
  Animation<T> animate(Animation<double> parent) 
    return _AnimatedEvaluation<T>(parent, this);
  

Tween 的基类 Animatable 除了能将动画进度转换成动画值之外,还提供了一个animate()方法,它的输入和输出都是 Animation,看上去像是把一个 Animation 转换成另一个 Animation,该方法的实现也是直接返回了一个_AnimatedEvaluation

class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> 
  // 在构造时注入父亲动画和 Animatable 对象
  _AnimatedEvaluation(this.parent, this._evaluatable);
  // 持有父亲动画
  @override
  final Animation<double> parent;
  // 持有 Animatable 对象
  final Animatable<T> _evaluatable;
  // 获取动画值:将父亲动画值转换为新动画值
  @override
  T get value => _evaluatable.evaluate(parent);

AnimatedEvaluation 继承自 Animation,并且它持有一个 Animation 实例。这就是典型的装饰者模式

关于装饰者模式的详细介绍可以点击 使用组合的设计模式 | 美颜相机中的装饰者模式 - 掘金 (juejin.cn)

装饰者模式的目的是扩展行为, AnimatedEvaluation 想扩展的行为是“获取动画值”,它把该行为委托给了一个 Animatable 对象。这就巧妙地实现了 “将父亲动画值转换为新动画值”

这里除了装饰者模式之外,还有适配器模式, AnimatedEvaluation 通过组合的方式持有 Animatable 实例并将其适配为 Animation。

Animation 是一个可以被监听的值,而 Animatable 提供了将动画值进行转换的功能。对于希望做动画的控件来说,它们希望和 Animation 对接,因为它可以被监听。但上层又希望能方便地变换动画值,所以 Animatable 被适配成了 Animation。

动画实例解析

结合一段控件做动画的代码,就能明白这样设计的妙处了:

class MyWidget extends StatefulWidget 
 @override
 _MyWidgetState createState() => _MyWidgetState();


class _MyWidgetState extends State<MyWidget> 
 @override
 Widget build(BuildContext context) 
   return Scaffold(
     appBar: AppBar(
       title: Text("Animation Demo"),
     ),
     body : Center(
       child: Container(
         width: 100,
         height: 100,
         color: Colors.red
       )
     )
   );
 

这是一个在屏幕中间固定宽高的红色矩形。来做一个让矩形变大的动画:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  // 动画控制器
  AnimationController controller; 
  // 变大动画
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   // 1.构造动画控制器
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   // 2.构造变大动画
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   // 3.开始动画
   controller.forward();
  

首先让 State 复用SingleTickerProviderStateMixin的行为:

mixin SingleTickerProviderStateMixin<T extends StatefulWidget> 
  on State<T> 
  implements TickerProvider  

SingleTickerProviderStateMixin 实现了 TickerProvider,这正是构造动画控制器必须的参数:

AnimationController(
  ...
  required TickerProvider vsync,
)

源码中以 Provider 命名的类通常都运用了抽象工厂设计模式,它将构建对象的细节抽象在一个接口中:

abstract class TickerProvider 
  @factory
  Ticker createTicker(TickerCallback onTick);

TickerProvider 想构建的对象是一个时钟 Ticker。

这样一来,动画控制器就无需关心构建时钟的细节:

AnimationController(
  ...
  required TickerProvider vsync,
) : _direction = _AnimationDirection.forward 
  _ticker = vsync.createTicker(_tick); // 构建时钟

无需关心细节意味着解耦,还有一个好处是动画控制器的上层类可以动态替换构建时钟的算法。关于工厂模式的详细介绍可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

构建完动画控制器后,就构建了 Tween 对象,并指定了动画值序列的起点和终点,紧接着就调用了animate()方法,这一步很关键:

sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);

这一步完成了动画进度到动画值序列的转换。其中 controller 承载着动画进度信息被作为参数传入。结合刚才的源码分析,controller 是一个 Animation 对象,在方法内部,它被装饰成另一个叫 _AnimatedEvaluation 的动画对象,该动画对象还持有了刚构建出来的 Tween,并把计算动画值的方法委托给了 Tween,完成了一次 Animatable 到 Animation 的适配

如此这般地华丽操作之后,调用 controller.forward() 开启动画,此时 sizeAnimation.value 就是变大动画对应的值序列。现在就要把它应用到控件的属性上:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   // 开始动画
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: Container(
           width: sizeAnimation.value, // 将动画值应用在宽度
           height: sizeAnimation.value, // 将动画值应用在高度
           color: Colors.red
         )
       )
     );
   

光是上面这段代码还不足以让控件动起来,因为动画控制器是一个可观察的值,但现在并没有控件观察它的变化:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
   // 监听动画值的变化并重绘控件
   controller.addListener(() 
     setState(() );
   )
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: Container(
           width: sizeAnimation.value, 
           height: sizeAnimation.value, 
           color: Colors.red
         )
       )
     );
   

这就是完整的动画代码。

封装动画代码

AnimatedWidget

Flutter 提供了一个控件,专门用于做动画,它封装了监听动画值这个操作:

abstract class AnimatedWidget extends StatefulWidget 

  const AnimatedWidget(
    Key? key,
    required this.listenable,
  ) : assert(listenable != null),
       super(key: key);

  // 可被监听的东西(其实就是AnimationController)
  final Listenable listenable;

  // 子类重载这个方法实现每一帧动画的重绘逻辑
  @protected
  Widget build(BuildContext context);

  @override
  State<AnimatedWidget> createState() => _AnimatedState();
  ...


class _AnimatedState extends State<AnimatedWidget> 
  @override
  void initState() 
    super.initState();
    // 添加监听器
    widget.listenable.addListener(_handleChange);
  

  // 更换监听器
  @override
  void didUpdateWidget(AnimatedWidget oldWidget) 
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) 
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    
  

  // 注销监听器
  @override
  void dispose() 
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  

  // 重绘控件
  void _handleChange() 
    setState(() 
    );
  
  // 将构建控件委托给widget.build()
  @override
  Widget build(BuildContext context) => widget.build(context);

然后就可以将动画组件单独抽象为一个控件:

class SizeTransition extends AnimatedWidget 
  SizeTransition(Key key, Animation<double> animation)
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context)
    return Center(
      child: Container(
        width: animation.value,
        height: animation.value, 
        color: Colors.red
     )
   );
  


class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : SizeTransition(animation: sizeAnimation)
     );
   

AnimatedBuilder

当页面中需要做动画的部分不太复杂的时候,也可以直接用 AnimatedBuilder 来进一步简化代码:

class AnimatedBuilder extends AnimatedWidget 
  const AnimatedBuilder(
    Key? key,
    required Listenable animation,
    required this.builder,
    this.child,
  ) : super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget? child;

  @override
  Widget build(BuildContext context) 
    return builder(context, child);
  


typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);

AnimatedBuilder 是 AnimatedWidget 的子类,它将构建动画控件的逻辑抽象在一个函数中,而且这个函数可以动态注入,这样就不需要像 AnimatedWidget 那样通过新建类并继承来实现动画了:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: AnimatedBuilder(
           animation: sizeAnimation,
           builder: (context, child) 
             return  Container(
               width: sizeAnimation.value, 
               height: sizeAnimation.value, 
               color: Colors.red
             );
           
         )
       )
     );
   

这就是典型的策略模式,它的目的是动态的替换行为。在这里如何构建控件被抽象为一组策略,上层代码可以动态地替换 AnimatedBuilder 构建控件的策略,这使得它和构建的细节解构。关于策略模式的详细分析可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

总结

读完 Flutter 关于动画的源码后,第一感觉就是“它把一个复杂问题拆分的很到位!”。

动画被拆分为三个独立的部分:

  1. 动画值序列 Tween
  2. 动画进度 AnimationController
  3. 绘制动画 Widget

其中 Tween 只关心如何生成动画值序列,AnimationController 负责监听渲染节奏并以此生成动画进度,而这两个概念和绘制解耦,即它们完全不知道动画会怎样呈现在屏幕上。因为呈现交给了 Widget 来处理。

Flutter 还运用了多种设计模式来增加动画代码的弹性,包括模板方法模式、策略模式、适配器模式、抽象工厂模式、装饰者模式。

(我什么时候才能写出这么优雅的代码?)

以上是关于动画动作分析基础知识的主要内容,如果未能解决你的问题,请参考以下文章

网易大咖分享之从Unity_RagDoll系统科普到动画全部基础原理

scratch中的知识帧动画怎么用?

Android进阶知识——Android动画深入分析

Android进阶知识——Android动画深入分析

Android进阶知识——Android动画深入分析

Spine应用--使用Spine动画制作动作游戏