流畅、有意义的动画对于移动应用用户体验来说是非常必要的。和react native的其他部分一样,动画api也还在积极开发中,不过我们已经可以联合使用两个互补的系统:用于全局的布局动画<code>layoutanimation</code>,和用于创建更精细的交互控制的动画<code>animated</code>。
<code>animated</code>库使得开发者可以非常容易地实现各种各样的动画和交互方式,并且具备极高的性能。<code>animated</code>仅关注动画的输入与输出声明,在其中建立一个可配置的变化函数,然后使用简单的<code>start/stop</code>方法来控制动画按顺序执行。下面是一个在加载时带有简单的弹跳动画的组件示例:
<code>bouncevalue</code>在构造函数中初始化为<code>state</code>的一部分,然后和图片的缩放比例进行绑定。在动画执行的背后,其数值会被不断的计算并用于设置缩放比例。当组件刚刚挂载的时候,缩放比例被设置到1.5。然后紧跟着在<code>bouncevalue</code>上执行了一个弹跳动画(spring),会逐帧刷新数值,并同步更新所有依赖本数值的绑定(在这个例子里,就是图片的缩放比例)。比起调用<code>setstate</code>然后重新渲染,这一运行过程要快得多。因为整个配置都是声明式的,我们可以实现更进一步的优化,只要序列化好配置,然后我们可以在一个高优先级的线程执行动画。
大部分你需要的东西都来自<code>animated</code>模块。它包括两个值类型,<code>value</code>用于单个的值,而<code>valuexy</code>用于向量值;还包括三种动画类型,<code>spring</code>,<code>decay</code>,还有<code>timing</code>,以及三种组件类型,<code>view</code>,<code>text</code>和<code>image</code>。你可以使用<code>animated.createanimatedcomponent</code>方法来对其它类型的组件创建动画。
这三种动画类型可以用来创建几乎任何你需要的动画曲线,因为它们每一个都可以被自定义:
<code>friction</code>: 摩擦力,默认为7.
<code>tension</code>: 张力,默认40。
<code>decay</code>: 以一个初始速度开始并且逐渐减慢停止。
<code>velocity</code>: 起始速度,必填参数。
<code>deceleration</code>: 速度衰减比例,默认为0.997。
<code>timing</code>: 从时间范围映射到渐变的值。
<code>duration</code>: 动画持续的时间(单位是毫秒),默认为500。
<code>easing</code>:一个用于定义曲线的渐变函数。阅读<code>easing</code>模块可以找到许多预定义的函数。ios默认为<code>easing.inout(easing.ease)</code>。
<code>delay</code>: 在一段时间之后开始动画(单位是毫秒),默认为0。
动画可以通过调用<code>start</code>方法来开始。<code>start</code>接受一个回调函数,当动画结束的时候会调用此回调函数。如果动画是因为正常播放完成而结束的,回调函数被调用时的参数为<code>{finished: true}</code>,但若动画是在结束之前被调用了<code>stop</code>而结束(可能是被一个手势或者其它的动画打断),它会收到参数<code>{finished: false}</code>。
多个动画可以通过<code>parallel</code>(同时执行)、<code>sequence</code>(顺序执行)、<code>stagger</code>和<code>delay</code>来组合使用。它们中的每一个都接受一个要执行的动画数组,并且自动在适当的时候调用start/stop。举个例子:
默认情况下,如果任何一个动画被停止或中断了,组内所有其它的动画也会被停止。parallel有一个<code>stoptogether</code>属性,如果设置为<code>false</code>,可以禁用自动停止。
<code>animated</code> api还有一个很强大的部分就是<code>interpolate</code>插值函数。它可以接受一个输入区间,然后将其映射到另一个的输出区间。下面是一个一个简单的从0-1区间到0-100区间的映射示例:
<code>interpolate</code>还支持定义多个区间段落,常用来定义静止区间等。举个例子,要让输入在接近-300时取相反值,然后在输入接近-100时到达0,然后在输入接近0时又回到1,接着一直到输入到100的过程中逐步回到0,最后形成一个始终为0的静止区间,对于任何大于100的输入都返回0。具体写法如下:
它的最终映射结果如下:
输入
输出
-400
450
-300
300
-200
150
-100
-50
0.5
1
50
100
101
200
<code>interpolation</code>还支持任意的渐变函数,其中有很多已经在<code>easing</code>类中定义了,包括二次、指数、贝塞尔等曲线以及step、bounce等方法。<code>interpolation</code>还支持限制输出区间<code>outputrange</code>。你可以通过设置<code>extrapolate</code>、<code>extrapolateleft</code>或<code>extrapolateright</code>属性来限制输出区间。默认值是<code>extend</code>(允许超出),不过你可以使用<code>clamp</code>选项来阻止输出值超过<code>outputrange</code>。
动画中所设的值还可以通过跟踪别的值得到。你只要把tovalue设置成另一个动态值而不是一个普通数字就行了。比如我们可以用弹跳动画来实现聊天头像的闪动,又比如通过<code>timing</code>设置<code>duration:0</code>来实现快速的跟随。他们还可以使用插值来进行组合:
<code>valuexy</code>是一个方便的处理2d交互的办法,譬如旋转或拖拽。它是一个简单的包含了两个<code>animated.value</code>实例的包装,然后提供了一系列辅助函数,使得<code>valuexy</code>在许多时候可以替代<code>value</code>来使用。比如在上面的代码片段中,<code>leader</code>和<code>follower</code>可以同时为<code>valuexy</code>类型,这样x和y的值都会被跟踪。
<code>animated.event</code>是animated api中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。第一层是一个数组,允许同时映射多个值,然后数组的每一个元素是一个嵌套的对象。在下面的例子里,你可以发现<code>scrollx</code>被映射到了<code>event.nativeevent.contentoffset.x</code>(<code>event</code>通常是回调函数的第一个参数),并且<code>pan.x</code>和<code>pan.y</code>分别映射到<code>gesturestate.dx</code>和<code>gesturestate.dy</code>(<code>gesturestate</code>是传递给<code>panresponder</code>回调函数的第二个参数)。
你可能会注意到这里没有一个明显的方法来在动画的过程中读取当前的值——这是出于优化的角度考虑,有些值只有在原生代码运行阶段中才知道。如果你需要在javascript中响应当前的值,有两种可能的办法:
<code>spring.stopanimation(callback)</code>会停止动画并且把最终的值作为参数传递给回调函数<code>callback</code>——这在处理手势动画的时候非常有用。
<code>spring.addlistener(callback)</code> 会在动画的执行过程中持续异步调用<code>callback</code>回调函数,提供一个最近的值作为参数。这在用于触发状态切换的时候非常有用,譬如当用户拖拽一个东西靠近的时候弹出一个新的气泡选项。不过这个状态切换可能并不会十分灵敏,因为它不像许多连续手势操作(如旋转)那样在60fps下运行。
如前面所述,我们计划继续优化animated,以进一步提升性能。我们还想尝试一些声明式的手势响应和触发动画,譬如垂直或者水平的倾斜操作。
<code>layoutanimation</code>允许你在全局范围内<code>创建</code>和<code>更新</code>动画,这些动画会在下一次渲染或布局周期运行。它常用来更新flexbox布局,因为它可以无需测量或者计算特定属性就能直接产生动画。尤其是当布局变化可能影响到父节点(譬如“查看更多”展开动画既增加父节点的尺寸又会将位于本行之下的所有行向下推动)时,如果不使用<code>layoutanimation</code>,可能就需要显式声明组件的坐标,才能使得所有受影响的组件能够同步运行动画。
注意尽管<code>layoutanimation</code>非常强大且有用,但它对动画本身的控制没有<code>animated</code>或者其它动画库那样方便,所以如果你使用<code>layoutanimation</code>无法实现一个效果,那可能还是要考虑其他的方案。
另外,如果要在android上使用layoutanimation,那么目前还需要在<code>uimanager</code>中启用:
<a target="_blank" href="https://rnplay.org/apps/uaqrgq">运行这个例子</a>
<code>requestanimationframe</code>是一个对浏览器标准api的兼容实现,你可能已经熟悉它了。它接受一个函数作为唯一的参数,并且在下一次重绘之前调用此函数。一些基于javascript的动画库高度依赖于这一api。通常你不必直接调用它——那些动画库会替你管理好帧的更新。
“补间是在两个图像之间生成中间帧的过程,以使得第一个图像能够平滑的变化为第二个图像”。补间帧是指在关键帧之间用于创建过渡假象的图画。”
这个库并未随react native一起发布——要在你的工程中使用它,则需要先在你的工程目录下执行<code>npm i react-tween-state --save</code>来安装。
<a target="_blank" href="https://rnplay.org/apps/4fuq-a">运行这个例子</a>
需要注意的是rebound动画可以被中断——如果你在按下动画的过程中释放手指,它会从当前状态弹回初始值。
你还可以为弹跳值启用边界,这样它们不会超出,而是会缓缓接近最终值。在上面的例子里,我们可以添加<code>this._scrollspring.setovershootclampingenabled(true)</code>来启用边界。参见下面的gif动画来看一个启用了边界的效果:
我们可以把这个用在rebound样例中来更新缩放比例——如果我们要更新的组件有一个非常深的内嵌结构,并且没有使用<code>shouldcomponentupdate</code>来优化,那么使用<code>setnativeprops</code>就将大有裨益。
<a target="_blank" href="https://rnplay.org/apps/fuqjag">运行这个例子</a>
不过你没办法把<code>setnativeprops</code>和react-tween-state结合使用,因为更新的补间值会自动被库设置到state上——rebound则不同,它通过<code>onsprintupdate</code>函数在每一帧中给我们提供一个更新后的值。
<a target="_blank" href="https://rnplay.org/apps/hpy6ua">运行这个例子</a>