上篇博客我們聊了RN中關於Timing的動畫,詳情請參見於《ReactNative之結合具體示例來看RN中的的Timing動畫》本篇博客我們將從一個“拉皮條”的一個動畫說起,然后來看一下RN中Spring動畫的使用方式以及具體效果。Spring從名字中不難看出是彈性彈簧的意思,也就是我們可以使用Spring這個動畫來實現一些彈性的動畫效果。本部分我們先通過一個“拉皮條”的示例來簡單的看一下Spring動畫的使用方式,然后在看一下Spring動畫中可配置的屬性以及每個屬性的作用。
一、從“拉皮條”談起
此拉皮條非彼“拉皮條”,此拉皮條是正經拉皮條,簡單的說,就是有一個皮條,我們用勁拉他,然后再松開觀察皮條的運行軌跡。下方就是我們“拉皮條”的示例,在這個“拉皮條”的示例中,我們主要使用了Animation中的Spring動畫。下方這個Demo中這個灰色的帶子就是我們要拉的皮條,一邊是黑色的固定皮條的東西,一端是可以拉動的紅色方框,我們往一邊拉動紅色方塊,這個皮條就會被拉伸,放手后皮條就會拉動我們的方塊到原位置,當然這個拉動的過程中是符合彈簧拉伸效果的。
下方是調整方塊質量的操作區,從下方效果中不難看出,當質量越大時慣性就越大,方塊來回擺動的幅度就越大,這也是符合彈簧的特性的。
效果就是這么個效果,接下來,我們來看一下上述效果的具體代碼實現,代碼也不算太多,下方會把核心的代碼拿出來聊聊。首先來看一下上述示例中用到的State。在State中有三個值,如下所示:
-
animationValue: 該值的類型為 Animated.ValueXY,ValueXY存放的是{x, y}的對象,其中這個x的值就是拖動后皮條拉伸后的X值,這個y值我們用來設置皮條的粗細度,也就是皮條的height。
-
mass: 然后就是這個mass(質量),我們用他來存放方塊的質量的。
-
moveX: 該值用來存放手指移動時的X值的,用作在移動時實時更新皮條的拉伸度以及方塊位置。
看完上述的State,接下來我們來看一下本Demo中涉及的手勢操作。下方的這個 DisplayView 就是整個皮條以及方塊所在的父View。下方是該View所涉及的手勢操作:
-
onStartShouldSetResponder: 首先通過該屬性開啟手勢相應者,在該屬性接收到方法中返回true來打開響應者。
-
onResponderMove: 該屬性所設置的方法就是是手指移動時所執行的回調,對應着iOS中的 touchMove 事件。通過該事件我們可以實時的拿到移動過程中的相關坐標。
-
onResponderRelease: 該屬性所對應的方法會在手指離開屏幕時觸發,我們可以在該事件中來打開 “皮條” 收縮的動畫。
而下方截圖中的這個 touchUp 事件就是手指離開屏幕時所觸發的動作。在該事件中,我們更新了 State 中的moveX,我們使用的是pageX,也就是相對應頁面的X值,這個MoveX我們設置的是方塊的中心位置,根據具體的布局,我們需要做個 45 的糾正,這個糾正后的值就是方塊要移動的地方。簡單的說也就是手指移動的地方就是方塊的中心點。設置完 MoveX 后,我們就開啟了Spring動畫,這個方塊就會隨着皮條的拉動往回走。
而這個 MoveView 方法就是隨着手指的移動試試的更新State中的MoveX的值,而方塊的位置就是根據這個State中MoveX的值決定的。
上述是我們本次動畫中所涉及的幾個事件,當然還有其他好多的手勢事件,以后有機會可以在其他博客中詳細的來介紹一下RN中常用的手勢操作,關於手勢在此就不做過多贅述了。
下方就是上述在 touchUp 方法中調用的啟動Spring動畫的相關方法,代碼比較簡單。就是設置了一下animation的目標值,及下方的animationValue, 以及設置了一下Spring動畫的配置對象,即下方的config對象,其中的 mass 就是本示例中方塊的質量。具體代碼如下所示:
下方代碼就是對應的就是紅色方塊的代碼實現,在該代碼中,我們為方塊動態設置了 left。在手動滑動時,這個left的值隨着手指移動的位置變化而變化,而當開始動畫時,這個Left的值對應的就是 animationValue 中的x的值。具體如下所示:
關於本次這個 “拉皮條” 的示例的介紹就先到這兒,畢竟篇幅有限,下方是上述示例的完整代碼:

二、“拉皮條” XS Max版本
Spring動畫有好多屬性,這些屬性對應着彈簧的各個物理特性,下方這個Demo 是上述“拉皮條”的一個升級版本,通過該Demo,我們可以很好的來觀察Spring動畫中各個屬性的作用,從而可以判斷相關屬性的各個使用場景。下方是“拉皮條” 的XS Max 版本。
備注:在上面第一個gif的最后有一個報錯,下方是具體的報錯內容,該錯誤的原因是我們設置的Spring的動畫屬性中沖突了。根據提示我們不難發現那些屬性會沖突。我們可以根據錯誤提示把屬性分為三組,(bounciness、speed ), (tenson、friction)以及(stiffness、damping、mass)如果設置了其中一個組的任何一個屬性,那么其他兩組中的屬性都能再設置了,因為設置完后違反彈簧相關的物理定律,是不合規的,所以會報錯。
You can define one of bounciness/speed, tenson/friction, or stiffness/damping/mass, but not more than one。
下方是該Demo中所涉及的屬性:
1、friction - 摩擦力
“摩擦摩擦,在光滑的地板上摩擦……”,關於什么是摩擦力就不多說了,因為大家都知道穿着滑板鞋在光滑的地板上摩擦~摩擦~。該屬性對應的就是滑塊的摩擦力,根據物理常識摩擦力越大滑塊被皮條拉伸的也就越慢,當摩擦力達到一定程度時,滑塊就是勻速的運動了,而不是拉不動的情況,下方是具體的表現效果:
2、tension - 張力
"張力,物理學名詞。物體受到拉力作用時,存在於其內部而垂直於兩鄰部分接觸面上的相互牽引力。", 額~上面就是張力的解釋,從物理字面量看,張力越大,方塊被拉回的速度也就越快。下方這個Demo就能體現出這一點。從下方的圖片中不難看出,隨着張力的逐漸增大,這個方塊被拉回的速度也就越快。
從上面的備注中我們可知,張力是可以和摩擦力一塊設置的,所以下方我們設置tension的時候,也選中了friction。摩擦力大的話會使張力對滑塊的作用力減小,這也是符合物理規律的。
3、bounciness - 抖
一個字兒概括就是“抖”,bounciness的值越大,這個滑塊被拉回來是抖的就越厲害。下方就是這個“抖”的具體示例,從下方不難看出這個抖的值越大,方塊回去時就越抖。
4、speed - 速度
速度及滑塊被“皮條”拉回的速度, 當這個 speed 的值越大時,滑塊就越容易被拉回,而且speed是可以和上面的“抖”bounciness一塊設置的。下方就是Speed的相關效果。
5、stiffness - 剛度
剛度這個玩意兒也是個物理名詞,剛度指材料或結構在受力時抵抗彈性變形的能力。通過這個解釋我們不難看出,剛度越大,說明彈簧越不容易變形,越不容易變形的情況下,如果拉伸后就越快的恢復原形。對於這個剛度可以簡單的理解為彈簧的剛度越好,那么這個彈簧的彈性就越好。下方就是剛度的表現:
6、damping - 阻尼
阻尼(damping) 的物理意義是力的衰減,或物體在運動中的能量耗散。通俗地講,就是阻止物體繼續運動。當物體受到外力作用而振動時,會產生一種使外力衰減的反力,稱為阻尼力(或減震力) 。換句話說,阻尼就是“減震”,作用就是用來防止物體來回抖動的,這個與上面聊的那個“抖” - bounciness 正好相反。阻尼越大,物體在運動過程中就越不抖,越小就抖的厲害。
阻尼的值必須大於零,而且阻尼可以與上面的剛度- stiffness 一塊設置。兩個阻尼相同,剛度越大抖的越厲害。
7、 mass - 質量
上面第一部分我們就聊質量了,物體的質量越大,慣性越大。同樣一根彈簧,質量越大就抖的越厲害。在Spring動畫中,stiffness(剛度)、damping(阻尼)和mass(質量)這三者是可以一塊設置的。具體效果如下所示:
8、delay - 延遲
這個就比較好理解了,就是在滑塊被皮條拉回去時的一個延遲,單位是毫秒。下方就是關於delay的演示。
上述就是RN中Spring中常用的配置參數了,可以根據不同的效果來具體設置不同的值。這些參數在不設置時也是有值的,下方是上述各個參數的默認值。
在本Demo中還用到了動畫的一個知識點,那就是同步執行動畫,一個是負責滑塊的動畫,一個負責皮條的動畫。
下方是該部分Demo的全部代碼,代碼不多也就200行左右。

1 import { 2 Animated, 3 TouchableOpacity, 4 View, 5 Text, 6 StyleSheet, 7 GestureResponderEvent 8 } from 'react-native' 9 import { Component } from 'react' 10 import React from 'react' 11 12 type States = { 13 animationValue: Animated.Value 14 heightValue: Animated.Value 15 configValue: any 16 configLineValue: any 17 moveX: number 18 } 19 20 // BorderView 21 export default class SpringAnimationView extends Component<null, States> { 22 isStartAnimation = false 23 configKey = [ 24 'friction', // 摩擦力 25 'tension', // 張力 26 'bounciness', // 彈性 27 'speed', // 速度 28 'stiffness', // 剛度 29 'damping', // 阻尼 30 'mass', // 質量 31 'delay' // 延遲 32 ] 33 34 // 各個參數的默認值 35 defaultValue = { 36 friction: 7, 37 tension: 40, 38 bounciness: 8, 39 speed: 12, 40 stiffness: 100, 41 damping: 10, 42 mass: 1, 43 delay: 0 44 } 45 46 constructor (props) { 47 super(props) 48 this.state = { 49 animationValue: new Animated.Value(0), 50 heightValue: new Animated.Value(0), 51 configValue: { }, 52 configLineValue: { }, 53 moveX: 30 54 } 55 } 56 57 // 拖動抬起時執行的回調方法 58 touchUp = (evt) => { 59 this.isStartAnimation = true 60 this.setState({ moveX: evt.nativeEvent.pageX - 45 }) 61 this.startAnimation() 62 } 63 64 // 移動View執行的方法 65 moveView = (evt: GestureResponderEvent) => { 66 this.isStartAnimation = false 67 this.setState({ moveX: evt.nativeEvent.pageX - 45 }) 68 } 69 70 // 開始動畫 71 startAnimation = () => { 72 this.state.animationValue.setValue(this.state.moveX) 73 this.state.heightValue.setValue(300 / this.state.moveX) 74 Animated.parallel([ 75 Animated.spring(this.state.animationValue, this.getConfigValue(30)), 76 Animated.spring(this.state.heightValue, this.getSecondConfigValue(10)) 77 ]).start() 78 } 79 80 // 獲取動畫執行的配置項 81 getConfigValue = (toValue: number) => { 82 let config = this.state.configValue 83 config.toValue = toValue 84 return config 85 } 86 87 // 獲取動畫執行的配置項 88 getSecondConfigValue = (toValue: number) => { 89 let config = this.state.configLineValue 90 config.toValue = toValue 91 return config 92 } 93 94 // 點擊配置項所執行的事件 95 clickConfigPress = (key: string) => () => { 96 let config = this.state.configValue 97 if (config[key] === undefined) { 98 config[key] = this.defaultValue[key] 99 } else { 100 config[key] = undefined 101 } 102 this.setState({ configValue: config, configLineValue: { ...config } }) 103 } 104 105 add = (key: string) => () => { 106 this.defaultValue[key] += 5 107 this.updateStateValue(key) 108 } 109 110 desc = (key: string) => () => { 111 this.defaultValue[key] -= 5 112 if (this.defaultValue[key] < 0) { 113 this.defaultValue[key] = 0 114 } 115 this.updateStateValue(key) 116 } 117 118 updateStateValue = (key: string) => { 119 let config = this.state.configValue 120 if (config[key] !== undefined) { 121 config[key] = this.defaultValue[key] 122 } 123 this.setState({ configValue: config }) 124 } 125 126 addOrDescView = (title: string, presse: () => void) => { 127 return ( 128 <TouchableOpacity onPress={presse}> 129 <View style={style.textView}> 130 <Text style={ style.textStyle}> {title} </Text> 131 </View> 132 </TouchableOpacity> 133 ) 134 } 135 136 configView = (key: string, index: number) => { 137 const { 138 configValue 139 } = this.state 140 let backgroundColor = '#000' 141 if (configValue[key] !== undefined) { 142 backgroundColor = '#f00' 143 } 144 return ( 145 <View key={index} style={{ flex: 1, flexDirection: 'row', height: 60 }}> 146 <TouchableOpacity onPress={this.clickConfigPress(key)}> 147 <View style={[style.textView, { backgroundColor: backgroundColor }]}> 148 <Text style={ style.textStyle}> {key} </Text> 149 </View> 150 </TouchableOpacity> 151 152 {this.addOrDescView('-', this.desc(key))} 153 154 <View style={[style.textView, { backgroundColor: '#fff' }]}> 155 <Text style={ [style.textStyle, { color: '#000' }]}> {this.defaultValue[key]} </Text> 156 </View> 157 {this.addOrDescView('+', this.add(key))} 158 </View> 159 ) 160 } 161 162 animatedView = () => { 163 let left: any = this.state.moveX 164 if (this.isStartAnimation) { 165 left = this.state.animationValue 166 } 167 return ( 168 <Animated.View 169 style={{ 170 height: 50, 171 width: 50, 172 left: left, 173 backgroundColor: '#f00', 174 position: 'absolute', 175 borderRadius: 10 176 }}/> 177 ) 178 } 179 180 displayView = () => { 181 let width: any = this.state.moveX 182 let height: any = 300 / this.state.moveX 183 if (this.isStartAnimation) { 184 width = this.state.animationValue 185 height = this.state.heightValue 186 } 187 return ( 188 <View style={style.displayView} 189 onStartShouldSetResponder={() => { return true }} 190 onResponderRelease={this.touchUp} 191 onResponderMove={this.moveView}> 192 <Animated.View style={{ height: height, width: width, backgroundColor: '#fff' }}/> 193 {this.animatedView()} 194 </View> 195 ) 196 } 197 198 render () { 199 return ( 200 <View style={{ flex: 1 , margin: 10 }}> 201 {/*拖動的View*/} 202 {this.displayView()} 203 204 {/*操作配置項的View*/} 205 { 206 this.configKey.map((key, index) => { 207 return this.configView(key, index) 208 }) 209 } 210 </View> 211 ) 212 } 213 } 214 215 const style = StyleSheet.create({ 216 textView: { 217 justifyContent: 'center', 218 alignItems: 'center', 219 height: 50, 220 backgroundColor: '#000', 221 margin: 10, 222 padding: 10, 223 borderRadius: 10, 224 borderWidth: 1 225 }, 226 textStyle: { 227 textAlignVertical: 'center', 228 color: '#fff' 229 }, 230 displayView: { 231 width: '100%', 232 height: 50, 233 backgroundColor: '#ccc', 234 borderLeftColor: '#000', 235 borderLeftWidth: 3, 236 borderBottomColor: '#000', 237 borderBottomWidth: 1, 238 flexDirection: 'row', 239 alignItems: 'center' 240 } 241 })
本篇的“拉皮條”的動畫就到這兒吧。