動畫
動畫對於創造優質的使用者體驗非常重要。靜止的物體在開始移動時必須克服慣性。運動中的物體具有動量,很少會立即停止。動畫讓您可以在介面中傳達物理上可信的運動。
React Native 提供了兩個互補的動畫系統:Animated
用於精細和互動式地控制特定值,以及 LayoutAnimation
用於動畫化的全域佈局轉換。
Animated
API
Animated
API 旨在以高效能的方式簡潔地表達各種有趣的動畫和互動模式。Animated
專注於輸入和輸出之間宣告式的關係,中間具有可配置的轉換,以及用於控制基於時間的動畫執行的 start
/stop
方法。
Animated
導出六種可動畫化的組件類型:View
、Text
、Image
、ScrollView
、FlatList
和 SectionList
,但您也可以使用 Animated.createAnimatedComponent()
建立自己的組件。
例如,一個在掛載時淡入的容器視圖可能如下所示
- TypeScript
- JavaScript
讓我們分解一下這裡發生的事情。在 FadeInView
的 render 方法中,使用 useRef
初始化了一個名為 fadeAnim
的新 Animated.Value
。View
上的 opacity 屬性映射到這個動畫值。在幕後,數值會被提取並用於設定不透明度。
當組件掛載時,不透明度會設定為 0。然後,在 fadeAnim
動畫值上啟動一個 easing 動畫,當值動畫化到最終值 1 時,它將在每一幀更新其所有依賴的映射(在本例中,只有不透明度)。
這是以優化的方式完成的,比調用 setState
和重新渲染更快。由於整個配置是宣告式的,我們將能夠實施進一步的優化,以序列化配置並在高優先級線程上運行動畫。
配置動畫
動畫是高度可配置的。自訂和預定義的 easing 函數、延遲、持續時間、衰減因子、彈簧常數等等都可以根據動畫的類型進行調整。
Animated
提供了幾種動畫類型,最常用的是 Animated.timing()
。它支援使用各種預定義的 easing 函數之一,或您可以使用自己的函數,在一段時間內對值進行動畫處理。Easing 函數通常用於動畫中,以傳達物體的逐漸加速和減速。
預設情況下,timing
將使用 easeInOut 曲線,該曲線傳達逐漸加速到全速,然後逐漸減速到停止。您可以通過傳遞 easing
參數來指定不同的 easing 函數。也支援自訂 duration
或甚至在動畫開始前的 delay
。
例如,如果我們想要建立一個 2 秒長的動畫,使物體在移動到最終位置之前稍微後退
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();
查看 配置動畫 部分的 Animated
API 參考,以了解有關內建動畫支援的所有配置參數的更多資訊。
組合動畫
動畫可以組合並按順序或並行播放。順序動畫可以在前一個動畫完成後立即播放,或者它們可以在指定的延遲後開始。Animated
API 提供了幾種方法,例如 sequence()
和 delay()
,每種方法都採用要執行的動畫陣列,並根據需要自動調用 start()
/stop()
。
例如,以下動畫滑行到停止,然後在並行旋轉時彈回
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // return to start
useNativeDriver: true,
}),
Animated.timing(twirl, {
// and twirl
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // start the sequence group
如果一個動畫停止或中斷,則組中的所有其他動畫也會停止。Animated.parallel
有一個 stopTogether
選項,可以將其設定為 false
以禁用此功能。
您可以在 組合動畫 部分的 Animated
API 參考中找到組合方法的完整列表。
組合動畫值
您可以通過加法、乘法、除法或模數 組合兩個動畫值,以建立新的動畫值。
在某些情況下,動畫值需要反轉另一個動畫值才能進行計算。一個例子是反轉比例 (2x --> 0.5x)
const a = new Animated.Value(1);
const b = Animated.divide(1, a);
Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();
內插
每個屬性都可以先通過內插來運行。內插將輸入範圍映射到輸出範圍,通常使用線性內插,但也支援 easing 函數。預設情況下,它將在外推給定的範圍之外的曲線,但您也可以讓它鉗制輸出值。
將 0-1 範圍轉換為 0-100 範圍的基本映射將是
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
例如,您可能希望將您的 Animated.Value
視為從 0 到 1,但將位置從 150px 動畫化到 0px,並將不透明度從 0 動畫化到 1。這可以通過修改上面範例中的 style
來完成,如下所示
style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}
interpolate()
也支援多個範圍段,這對於定義死區和其他方便的技巧非常有用。例如,要獲得在 -300 處的否定關係,在 -100 處變為 0,然後在 0 處回到 1,然後在 100 處回到零,然後是一個死區,對於超出該範圍的所有內容都保持在 0,您可以執行
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
這將像這樣映射
Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0
interpolate()
也支援映射到字串,允許您對顏色以及帶單位的數值進行動畫處理。例如,如果您想對旋轉進行動畫處理,您可以執行
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});
interpolate()
也支援任意 easing 函數,其中許多函數已在 Easing
模組中實現。interpolate()
還具有用於外推 outputRange
的可配置行為。您可以通過設定 extrapolate
、extrapolateLeft
或 extrapolateRight
選項來設定外推。預設值為 extend
,但您可以使用 clamp
來防止輸出值超過 outputRange
。
追蹤動態值
動畫值還可以通過將動畫的 toValue
設定為另一個動畫值而不是純數字來追蹤其他值。例如,Android 上 Messenger 使用的「聊天大頭貼」動畫可以使用釘在另一個動畫值上的 spring()
,或使用 timing()
和 duration
為 0 的剛性追蹤來實現。它們也可以與內插組合
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();
leader
和 follower
動畫值將使用 Animated.ValueXY()
實現。ValueXY
是一種處理 2D 互動(例如平移或拖曳)的便捷方式。它是一個基本包裝器,包含兩個 Animated.Value
實例和一些調用它們的輔助函數,使 ValueXY
在許多情況下可以替代 Value
。它允許我們在上面的範例中追蹤 x 和 y 值。
追蹤手勢
手勢(例如平移或滾動)和其他事件可以使用 Animated.event
直接映射到動畫值。這是通過結構化的映射語法完成的,以便可以從複雜的事件物件中提取值。第一層是一個陣列,允許跨多個參數進行映射,並且該陣列包含巢狀物件。
例如,當使用水平滾動手勢時,您需要執行以下操作,以便將 event.nativeEvent.contentOffset.x
映射到 scrollX
(一個 Animated.Value
)
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
以下範例實現了一個水平滾動輪播,其中滾動位置指示器使用 ScrollView
中使用的 Animated.event
進行動畫處理
帶有動畫事件範例的 ScrollView
當使用 PanResponder
時,您可以使用以下程式碼從 gestureState.dx
和 gestureState.dy
中提取 x 和 y 位置。我們在陣列的第一個位置使用 null
,因為我們只對傳遞給 PanResponder
處理程式的第二個參數 gestureState
感興趣。
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}
帶有動畫事件範例的 PanResponder
回應目前的動畫值
您可能會注意到,在動畫處理時,沒有明確的方法可以讀取目前的值。這是因為該值可能僅在原生運行時中知道,這是由於優化。如果您需要在 JavaScript 中響應目前的值運行,則有兩種方法
spring.stopAnimation(callback)
將停止動畫,並使用最終值調用callback
。這在進行手勢轉換時很有用。spring.addListener(callback)
將在動畫運行時異步調用callback
,提供最近的值。這對於觸發狀態更改非常有用,例如在使用者將搖擺物拖動到更近的位置時,將其捕捉到新選項,因為與需要以 60 fps 運行的連續手勢(如平移)相比,這些較大的狀態更改對幾幀的延遲不太敏感。
Animated
旨在完全可序列化,以便動畫可以以高效能的方式運行,獨立於正常的 JavaScript 事件循環。這確實會影響 API,因此當與完全同步的系統相比,做某些事情似乎有點棘手時,請記住這一點。查看 Animated.Value.addListener
作為解決其中一些限制的方法,但請謹慎使用它,因為它將來可能會對效能產生影響。
使用原生驅動程式
Animated
API 旨在可序列化。通過使用 原生驅動程式,我們在開始動畫之前將有關動畫的所有內容發送到原生,從而允許原生程式碼在 UI 線程上執行動畫,而無需在每一幀上都通過橋接器。動畫開始後,JS 線程可以被阻止,而不會影響動畫。
對於普通動畫,可以使用原生驅動程式,方法是在啟動動畫時在動畫配置中設定 useNativeDriver: true
。沒有 useNativeDriver
屬性的動畫預設為 false,這是由於舊版原因,但會發出警告(以及 TypeScript 中的類型檢查錯誤)。
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();
動畫值僅與一個驅動程式兼容,因此如果您在使用原生驅動程式啟動值動畫時,請確保該值上的每個動畫也使用原生驅動程式。
原生驅動程式也適用於 Animated.event
。這對於跟隨滾動位置的動畫特別有用,因為如果沒有原生驅動程式,由於 React Native 的異步性質,動畫將始終比手勢落後一幀。
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Set this to true
)}>
{content}
</Animated.ScrollView>
您可以通過運行 RNTester 應用程式,然後載入 Native Animated Example 來查看原生驅動程式的實際效果。您也可以查看 原始碼,以了解這些範例是如何產生的。
注意事項
並非所有您可以使用 Animated
執行的操作目前都受原生驅動程式支援。主要的限制是您只能對非佈局屬性進行動畫處理:transform
和 opacity
之類的東西可以使用,但 Flexbox 和 position 屬性則不行。當使用 Animated.event
時,它僅適用於直接事件,而不適用於冒泡事件。這意味著它不適用於 PanResponder
,但適用於 ScrollView#onScroll
之類的東西。
當動畫正在運行時,它可能會阻止 VirtualizedList
組件渲染更多行。如果您需要在使用者滾動列表時運行長時間或循環動畫,您可以在動畫的配置中使用 isInteraction: false
來防止此問題。
請記住
雖然使用 transform 樣式(例如 rotateY
、rotateX
等),但請確保 transform 樣式 perspective
已就位。目前,某些動畫可能在沒有它的情況下無法在 Android 上渲染。範例如下。
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // without this line this Animation will not render on Android while working fine on iOS
],
}}
/>
其他範例
RNTester 應用程式有各種 Animated
使用範例
LayoutAnimation
API
LayoutAnimation
允許您全域配置 create
和 update
動畫,這些動畫將用於下一個渲染/佈局週期中的所有視圖。這對於執行 Flexbox 佈局更新非常有用,而無需費心測量或計算特定屬性以便直接對它們進行動畫處理,並且當佈局更改可能影響祖先時尤其有用,例如「查看更多」展開,這也會增加父級的大小並向下推動下方的行,否則將需要組件之間的顯式協調才能同步動畫它們。
請注意,雖然 LayoutAnimation
非常強大並且可能非常有用,但它提供的控制比 Animated
和其他動畫庫少得多,因此如果無法讓 LayoutAnimation
執行您想要的操作,您可能需要使用另一種方法。
請注意,為了使其在 Android 上工作,您需要通過 UIManager
設定以下標誌
UIManager.setLayoutAnimationEnabledExperimental(true);
此範例使用預設值,您可以根據需要自訂動畫,有關更多資訊,請參閱 LayoutAnimation.js。
其他注意事項
requestAnimationFrame
requestAnimationFrame
是瀏覽器中的 polyfill,您可能很熟悉。它接受一個函數作為其唯一參數,並在下一次重繪之前調用該函數。它是動畫的基本構建模組,所有基於 JavaScript 的動畫 API 都基於它。一般來說,您不應該需要自己調用此函數 - 動畫 API 將為您管理幀更新。
setNativeProps
如 直接操作部分 中所述,setNativeProps
允許我們直接修改原生支援的組件(實際上由原生視圖支援的組件,與複合組件不同)的屬性,而無需 setState
和重新渲染組件層次結構。
我們可以在 Rebound 範例中使用它來更新比例 - 如果我們要更新的組件是深度巢狀的並且尚未通過 shouldComponentUpdate
進行優化,這可能會有所幫助。
如果您發現動畫掉幀(效能低於每秒 60 幀),請研究使用 setNativeProps
或 shouldComponentUpdate
來優化它們。或者,您可以 使用 useNativeDriver 選項 在 UI 線程而不是 JavaScript 線程上運行動畫。您可能還希望將任何計算密集型工作延遲到動畫完成後,使用 InteractionManager。您可以使用應用內開發選單「FPS 監視器」工具來監視幀率。