跳到主要內容

動畫

動畫對於創造優質的使用者體驗非常重要。靜止的物體在開始移動時必須克服慣性。運動中的物體具有動量,很少會立即停止。動畫讓您可以在介面中傳達物理上可信的運動。

React Native 提供了兩個互補的動畫系統:Animated 用於精細和互動式地控制特定值,以及 LayoutAnimation 用於動畫化的全域佈局轉換。

Animated API

Animated API 旨在以高效能的方式簡潔地表達各種有趣的動畫和互動模式。Animated 專注於輸入和輸出之間宣告式的關係,中間具有可配置的轉換,以及用於控制基於時間的動畫執行的 start/stop 方法。

Animated 導出六種可動畫化的組件類型:ViewTextImageScrollViewFlatListSectionList,但您也可以使用 Animated.createAnimatedComponent() 建立自己的組件。

例如,一個在掛載時淡入的容器視圖可能如下所示

讓我們分解一下這裡發生的事情。在 FadeInView 的 render 方法中,使用 useRef 初始化了一個名為 fadeAnim 的新 Animated.ValueView 上的 opacity 屬性映射到這個動畫值。在幕後,數值會被提取並用於設定不透明度。

當組件掛載時,不透明度會設定為 0。然後,在 fadeAnim 動畫值上啟動一個 easing 動畫,當值動畫化到最終值 1 時,它將在每一幀更新其所有依賴的映射(在本例中,只有不透明度)。

這是以優化的方式完成的,比調用 setState 和重新渲染更快。由於整個配置是宣告式的,我們將能夠實施進一步的優化,以序列化配置並在高優先級線程上運行動畫。

配置動畫

動畫是高度可配置的。自訂和預定義的 easing 函數、延遲、持續時間、衰減因子、彈簧常數等等都可以根據動畫的類型進行調整。

Animated 提供了幾種動畫類型,最常用的是 Animated.timing()。它支援使用各種預定義的 easing 函數之一,或您可以使用自己的函數,在一段時間內對值進行動畫處理。Easing 函數通常用於動畫中,以傳達物體的逐漸加速和減速。

預設情況下,timing 將使用 easeInOut 曲線,該曲線傳達逐漸加速到全速,然後逐漸減速到停止。您可以通過傳遞 easing 參數來指定不同的 easing 函數。也支援自訂 duration 或甚至在動畫開始前的 delay

例如,如果我們想要建立一個 2 秒長的動畫,使物體在移動到最終位置之前稍微後退

tsx
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();

查看 配置動畫 部分的 Animated API 參考,以了解有關內建動畫支援的所有配置參數的更多資訊。

組合動畫

動畫可以組合並按順序或並行播放。順序動畫可以在前一個動畫完成後立即播放,或者它們可以在指定的延遲後開始。Animated API 提供了幾種方法,例如 sequence()delay(),每種方法都採用要執行的動畫陣列,並根據需要自動調用 start()/stop()

例如,以下動畫滑行到停止,然後在並行旋轉時彈回

tsx
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)

tsx
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 範圍的基本映射將是

tsx
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

例如,您可能希望將您的 Animated.Value 視為從 0 到 1,但將位置從 150px 動畫化到 0px,並將不透明度從 0 動畫化到 1。這可以通過修改上面範例中的 style 來完成,如下所示

tsx
  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,您可以執行

tsx
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() 也支援映射到字串,允許您對顏色以及帶單位的數值進行動畫處理。例如,如果您想對旋轉進行動畫處理,您可以執行

tsx
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate() 也支援任意 easing 函數,其中許多函數已在 Easing 模組中實現。interpolate() 還具有用於外推 outputRange 的可配置行為。您可以通過設定 extrapolateextrapolateLeftextrapolateRight 選項來設定外推。預設值為 extend,但您可以使用 clamp 來防止輸出值超過 outputRange

追蹤動態值

動畫值還可以通過將動畫的 toValue 設定為另一個動畫值而不是純數字來追蹤其他值。例如,Android 上 Messenger 使用的「聊天大頭貼」動畫可以使用釘在另一個動畫值上的 spring(),或使用 timing()duration 為 0 的剛性追蹤來實現。它們也可以與內插組合

tsx
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
useNativeDriver: true,
}).start();

leaderfollower 動畫值將使用 Animated.ValueXY() 實現。ValueXY 是一種處理 2D 互動(例如平移或拖曳)的便捷方式。它是一個基本包裝器,包含兩個 Animated.Value 實例和一些調用它們的輔助函數,使 ValueXY 在許多情況下可以替代 Value。它允許我們在上面的範例中追蹤 x 和 y 值。

追蹤手勢

手勢(例如平移或滾動)和其他事件可以使用 Animated.event 直接映射到動畫值。這是通過結構化的映射語法完成的,以便可以從複雜的事件物件中提取值。第一層是一個陣列,允許跨多個參數進行映射,並且該陣列包含巢狀物件。

例如,當使用水平滾動手勢時,您需要執行以下操作,以便將 event.nativeEvent.contentOffset.x 映射到 scrollX(一個 Animated.Value

tsx
 onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}

以下範例實現了一個水平滾動輪播,其中滾動位置指示器使用 ScrollView 中使用的 Animated.event 進行動畫處理

帶有動畫事件範例的 ScrollView

當使用 PanResponder 時,您可以使用以下程式碼從 gestureState.dxgestureState.dy 中提取 x 和 y 位置。我們在陣列的第一個位置使用 null,因為我們只對傳遞給 PanResponder 處理程式的第二個參數 gestureState 感興趣。

tsx
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 中的類型檢查錯誤)。

tsx
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();

動畫值僅與一個驅動程式兼容,因此如果您在使用原生驅動程式啟動值動畫時,請確保該值上的每個動畫也使用原生驅動程式。

原生驅動程式也適用於 Animated.event。這對於跟隨滾動位置的動畫特別有用,因為如果沒有原生驅動程式,由於 React Native 的異步性質,動畫將始終比手勢落後一幀。

tsx
<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 執行的操作目前都受原生驅動程式支援。主要的限制是您只能對非佈局屬性進行動畫處理:transformopacity 之類的東西可以使用,但 Flexbox 和 position 屬性則不行。當使用 Animated.event 時,它僅適用於直接事件,而不適用於冒泡事件。這意味著它不適用於 PanResponder,但適用於 ScrollView#onScroll 之類的東西。

當動畫正在運行時,它可能會阻止 VirtualizedList 組件渲染更多行。如果您需要在使用者滾動列表時運行長時間或循環動畫,您可以在動畫的配置中使用 isInteraction: false 來防止此問題。

請記住

雖然使用 transform 樣式(例如 rotateYrotateX 等),但請確保 transform 樣式 perspective 已就位。目前,某些動畫可能在沒有它的情況下無法在 Android 上渲染。範例如下。

tsx
<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 允許您全域配置 createupdate 動畫,這些動畫將用於下一個渲染/佈局週期中的所有視圖。這對於執行 Flexbox 佈局更新非常有用,而無需費心測量或計算特定屬性以便直接對它們進行動畫處理,並且當佈局更改可能影響祖先時尤其有用,例如「查看更多」展開,這也會增加父級的大小並向下推動下方的行,否則將需要組件之間的顯式協調才能同步動畫它們。

請注意,雖然 LayoutAnimation 非常強大並且可能非常有用,但它提供的控制比 Animated 和其他動畫庫少得多,因此如果無法讓 LayoutAnimation 執行您想要的操作,您可能需要使用另一種方法。

請注意,為了使其在 Android 上工作,您需要通過 UIManager 設定以下標誌

tsx
UIManager.setLayoutAnimationEnabledExperimental(true);

此範例使用預設值,您可以根據需要自訂動畫,有關更多資訊,請參閱 LayoutAnimation.js

其他注意事項

requestAnimationFrame

requestAnimationFrame 是瀏覽器中的 polyfill,您可能很熟悉。它接受一個函數作為其唯一參數,並在下一次重繪之前調用該函數。它是動畫的基本構建模組,所有基於 JavaScript 的動畫 API 都基於它。一般來說,您不應該需要自己調用此函數 - 動畫 API 將為您管理幀更新。

setNativeProps

直接操作部分 中所述,setNativeProps 允許我們直接修改原生支援的組件(實際上由原生視圖支援的組件,與複合組件不同)的屬性,而無需 setState 和重新渲染組件層次結構。

我們可以在 Rebound 範例中使用它來更新比例 - 如果我們要更新的組件是深度巢狀的並且尚未通過 shouldComponentUpdate 進行優化,這可能會有所幫助。

如果您發現動畫掉幀(效能低於每秒 60 幀),請研究使用 setNativePropsshouldComponentUpdate 來優化它們。或者,您可以 使用 useNativeDriver 選項 在 UI 線程而不是 JavaScript 線程上運行動畫。您可能還希望將任何計算密集型工作延遲到動畫完成後,使用 InteractionManager。您可以使用應用內開發選單「FPS 監視器」工具來監視幀率。