React native 动画详解
参考文档 :动画概述、动画API、动画缓动函数、interactionmanager
文档其实已经比较详细了,这里整理一下,做个笔记,也算加深一下自己的印象。
定义
所谓动画,就是 UI 上的变化,而在 RN 中,UI 对应的就是 style,那么动画就是要顺滑的改变 style 某个属性的值,就以 opacity 来作为解释吧
如果想要实现透明度的不断变化,比如可以用 state 中定义的属性作为 style.opacity 的值,后续通过 requestAnimationFrame 来不停修改 state 值,可以做到,但性能低下,这就是 RN Animated 存在的意义
const opacity = new Animated.Value(0);
<Animated.View style={{
opacity: opacity
}}/>
这就好了,opacity
的属性值为一个 Animated.Value
,当需要修改透明度的了,不需要改 state, 而是重置
Animated.Value
的值就行了,他会实时传导给 View 的 style.opacity
动起来
如何修改 Animated.Value
的值
// 定义 opacity 的值如何变化
const an = Animated.timing(opacity, {
toValue: 1, // 最终变为 1
delay: 1000, // 延迟 1 秒后开始变换(在此期间,opacity 值仍为 0)
duration: 1000, // 用 1 秒的时间去变换(在 1 秒内,opacity 由 0 -> 1)
easing: Easing.back(), // 变换过程不一定是均匀变换,可指明变换函数
useNativeDriver:false, // 是否启用原生驱动
isInteraction:true, // 是否在InteractionManager的队列中注册,后面会提到
})
// 开始动画 (可选:设置执行完成的回调)
an.start([callback]);
// 停下动画
an.stop()
说下配置项中的三个参数
const a = new Animated.Value(0);
Animated.timing(opacity, {
toValue: a,
useNativeDriver:false,
isInteraction:true,
})
toValue
可以指定为其他 Animated.Value 动态值,而不是固定值
useNativeDriver
启用原生使用ui线程,可提供动画流畅度,但有限制
1)启用了原生,那么和该动画相关的其他动画也必须是原生
2)不支持用在 style 的盒模型属性上,启用原生驱动,最安全的就是 opacity 和 transform
isInteraction
RN 有一个 interactionmanager API,可执行一些耗时任务,这些任务会在动画执行完毕后再启动,通过该属性来指明是否支持该 API, 默认为 true,即动画完事再执行任务,设为 false 则不然
除了 Animated.timing,还有两个已经预置了 easing 效果的方法:Animated.decay、Animated.spring
合成
假设我们要做一个,由透明逐渐显示、且宽度从 0 变到 100 的 view,透明度就是上面的办法了,那么宽度呢,我们可以重新定义一个
const width = new Animated.Value(0);
const w = Animated.timing(width, {
toValue: 100,
...... // 其他配置,与 opacity 保持一致
})
w.start()
这样搞,配置就要写两遍,当然,也可以把配置提出来用一个变量来定义,这样写一遍就好了,但这样,要同时调用两个动画的 start() ,两个执行可能不完全同步,这就是合成的意义了,可以这样写
const opacity = new Animated.Value(0);
const width = Animated.multiply(opacity, 100);
<View style={{
opacity: opacity,
width:width
}}/>
Animated.timing(opacity, {
...
}).start()
使用合成,只需要启动 opacity
就好了,width
的值会自动跟着 opacity
变化,妙啊~
RN 提供了 Animated.add (加)、Animated.subtract (减)、Animated.multiply(乘)、Animated.divide(除)、Animated.modulo(取余) 五个合成方法
插值
上面的合成函数虽然方便,但实际操作中远不止加减乘除这么简单,那么能不能自定义合成函数呢?答案是不太能,因为自定义函数意味着,动画跑起来之后,还要和 js 交互,来拿计算结果,在性能上是不能接受的,卡成幻灯片的动画还叫动画吗,也许未来某一天,RN 可以静态编译 js 无状态纯函数就可以了,那在那一天到之前,插值是当前的选择
const opacity = new Animated.Value(0);
const width = opacity.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 20, 100]
});
<View style={{
opacity: opacity,
width:width
}}/>
Animated.timing(opacity, {
...
}).start()
插值通过 interpolate
将输入 inputRange
映射为 outputRange
,可分段映射,这样灵活性大大提高
组合
产品改需求了,先逐渐显示,然后再逐渐变宽。好简单,在 opacity
的 start()
加回调 -> 启动 width
的 start()
;可以,但如果有 N 个连续动画,回调恐怕会吐血,这就需要组合动画了
Animated.sequence
按顺序执行一系列动画
Animated.delay
在顺序执行动画时,可以让两个动画衔接时,延迟一会
Animated.sequence([
Animated.timing(opacity, {
...
})
Animated.delay(1000); // 有点像 sleep
Animated.timing(width, {
...
})
]).start()
假如希望 opacity
和 width
同时执行,但动画过程不是一个步调,使用不同的 easing 或 duraition
Animated.parallel
同时执行一系列动画
Animated.stagger
延迟启动一系列动画,基本等同于 setTimeout(Animated.parallel)
Animated.parallel([
Animated.timing(opacity, {
...
})
Animated.timing(width, {
...
})
]).start()
Animated.stagger(1000, [
Animated.timing(opacity, {
...
})
Animated.timing(width, {
...
})
]).start()
Animated.loop
循环执行动画
可以在 start 回调中再启动自身,这样也能搞一个循环动画,但这样的话,每次结束,是 ui 线程调用 js, 再唤起 ui,周而复始,不如直接 loop 性能来的好
Animated.loop(Animated.timing(opacity, {
...
isInteraction:false
}), {
iterations:-1
})
- 函数的第二个参数是设置循环次数的,可省略
- 建议配置 isInteraction=false 是因为这是循环动画,会导致其他任务永远无法执行了
交互
以上都是提前设计好动画,然后执行,在实际使用中动画,其实就是 style 可能是需要根据用户操作来进行变动的,比如这样的组件 react-native-parallax-scroll-view,这就需要用到 Animated.event
函数了
// argMapping 是映射配置
// config 接受两个参数, js 异步回调函数 和 是否启用原生驱动
Animated.event(argMapping, {
listener:Function,
useNativeDriver:true
})
需要先强调一件事,否则看 argMapping 恐怕会有点懵
Animated 的本质是提供输入值,比如上面通过 初始value
和 配置toValue
明确提供一个区间值作为输入值,亦或通过 RN 提供的合成或插值将区间值进行调整,说白了,就是不希望 js 内进行任何计算,要么提供明确的值,要么使用 RN 的计算手段。
而交互呢,其实就是将 RN 提供的值,再作为输入值,还是不让 js 参合,虽然提供了 listener 回调,但也只是让你知道当前执行到哪了,是不能进行修改的,并且这个回调的值可能还有延迟。argMapping 就是将 RN 提供的输出值 映射 到一个动画上作为输入值。
原生交互,也就是手势交互,只有一个 panresponder,还有就是 scrollview 二次封装的 onScroll,当然,也可能有其他三方组件再次封装,但原理都是一样的
以 panresponder
举例,绑定监听函数会返回两个参数,具体看官网
onPanResponderMove: (nativeEvent, gestureState) => {}
gestureState = {
....
dx - 从触摸操作开始时的累计横向路程
dy - 从触摸操作开始时的累计纵向路程
}
比如我们现在要将 dx 映射到某个 Animated.value
const x = new Animated.Value(0);
onPanResponderMove: Animated.event([
null, { dx: x }
])
嗯,这就完事了, event([])
中的 []
会传递回调函数参数,第一个不用,直接 null ,然后映射第二个,就是 gestureState.dx -> x
,当然,你可以从 gestureState 找其他参数进行映射
为加深理解,再看看 onScroll
, 根据介绍,其函数原型是这样的
onScroll : (event) => {}
event = {
nativeEvent: {
contentInset: { bottom, left, right, top },
contentOffset: { x, y },
contentSize: { height, width },
layoutMeasurement: { height, width },
zoomScale
}
}
比如要映射 Y 轴滚动距离
const dy = new Animated.Value(0);
onScroll={Animated.event(
[{ nativeEvent: {
contentOffset: { y: dy }
}}]
)}
嗯,就是这样,参数对参数
响应
输入值 虽然无法自定义函数进行计算,但好歹提供了 合成 和 插值的办法,输出值,就是当前的 Animated.Value 实际值,有么有办法监听?
RN 提供了一些方法,具体可参见 animatedvalue、animatedvaluexy;使用方法比较简单
const an = new Animated.Value(0);
an.addListener(v => {
});
// 也可直接设置值
an.setValue(2)
这种响应是不建议直接参与动画的,适合作为旁观者静静看着动画,当达到某一个符合条件的值,触发一些其他操作。
收尾
最后再加上 layoutanimation 便是 RN动画 的全部内容了,该 API 是做整体动画的,不是特别常用,就没展开,需要的话可以自行查阅。
本篇文档都是使用 RN 中文网的链接,该网站是第三方进行翻译的,质量不错,但相对于 官网 还是有所不足,比如上面说的 animatedvalue
API,由于官网上没有相关入口,所以中文网也没有,但在官网顶部的 search 中是可以搜索到的,中文网则搜索不到 :) 英文水平跟得上,直接看官方文档,更新会比较及时
最后,就是 github 上的 react-native-website ,也就是官网的的托管库,其实也是一个不错的寻找文档的地方。
最最后,这里还有一个不错的学习资料:https://future-challenger.gitbooks.io/react-native-animation/content/
最最最后,官方推荐的导航组件 react-navigation 使用一个第三方动画库 react-native-reanimated,该库更为强大,这里就不再介绍了,有兴趣的朋友可以研究一下