跳到主要內容

使用原生驅動程式進行動畫

·7 分鐘閱讀
Janic Duplessis
App & Flow 的軟體工程師

在過去一年中,我們一直致力於改善使用 Animated 函式庫的動畫效能。動畫對於創造美好的使用者體驗非常重要,但也很難做得正確。我們希望讓開發人員能夠輕鬆建立高效能的動畫,而不必擔心某些程式碼會導致動畫延遲。

這是什麼?

Animated API 的設計考慮到一個非常重要的限制,即它是可序列化的。這表示我們可以在動畫開始之前將所有關於動畫的資訊傳送給原生端,並允許原生程式碼在 UI 執行緒上執行動畫,而無需在每個影格都通過橋接器。這非常有用,因為一旦動畫開始,JS 執行緒可能會被封鎖,而動畫仍然可以順暢執行。實際上,這種情況經常發生,因為使用者程式碼在 JS 執行緒上執行,而 React 渲染也可能會長時間鎖定 JS。

一點歷史...

這個專案大約在一年前開始,當時 Expo 在 Android 上建置了 li.st 應用程式。Krzysztof Magiera 受聘在 Android 上建置初始實作。最終效果良好,li.st 是第一個使用 Animated 出貨原生驅動動畫的應用程式。幾個月後,Brandon Withrow 在 iOS 上建置了初始實作。在那之後,Ryan Gomba 和我致力於新增遺失的功能,例如支援 Animated.event,以及修正我們在生產應用程式中使用時發現的錯誤。這真的是社群共同努力的成果,我要感謝所有參與者以及 Expo 對於贊助大部分開發工作的支持。React Native 中的 Touchable 組件以及新發布的 React Navigation 函式庫中的導覽動畫現在都使用它。

它是如何運作的?

首先,讓我們看看目前使用 Animated 和 JS 驅動程式的動畫是如何運作的。當使用 Animated 時,您宣告一個節點圖,表示您要執行的動畫,然後使用驅動程式使用預定義的曲線更新 Animated 值。您也可以使用 Animated.event 將 Animated 值連接到 View 的事件來更新它。

以下是動畫步驟及其發生位置的細分

  • JS:動畫驅動程式使用 requestAnimationFrame 在每個影格上執行,並使用根據動畫曲線計算的新值更新它驅動的值。
  • JS:計算中間值並將其傳遞給附加到 View 的 props 節點。
  • JS:使用 setNativeProps 更新 View
  • JS 到原生橋接器。
  • 原生:更新 UIViewandroid.View

如您所見,大部分工作都在 JS 執行緒上發生。如果它被封鎖,動畫將會跳過影格。它也需要在每個影格都通過 JS 到原生橋接器來更新原生檢視。

原生驅動程式的作用是將所有這些步驟移至原生端。由於 Animated 產生動畫節點圖,因此可以在動畫開始時將其序列化並僅傳送給原生端一次,從而無需回呼到 JS 執行緒;原生程式碼可以負責在每個影格直接在 UI 執行緒上更新檢視。

以下是如何序列化動畫值和內插節點的範例(不是確切的實作,僅為範例)。

建立原生值節點,這是將要動畫化的值

NativeAnimatedModule.createNode({
id: 1,
type: 'value',
initialValue: 0,
});

建立原生內插節點,這會告訴原生驅動程式如何內插值

NativeAnimatedModule.createNode({
id: 2,
type: 'interpolation',
inputRange: [0, 10],
outputRange: [10, 0],
extrapolate: 'clamp',
});

建立原生 props 節點,這會告訴原生驅動程式它附加到的檢視上的哪個 prop

NativeAnimatedModule.createNode({
id: 3,
type: 'props',
properties: ['style.opacity'],
});

將節點連接在一起

NativeAnimatedModule.connectNodes(1, 2);
NativeAnimatedModule.connectNodes(2, 3);

將 props 節點連接到檢視

NativeAnimatedModule.connectToView(3, ReactNative.findNodeHandle(viewRef));

有了這些,原生動畫模組就擁有更新原生檢視所需的所有資訊,而無需前往 JS 計算任何值。

剩下要做的就是實際啟動動畫,方法是指定我們想要的動畫曲線類型以及要更新的動畫值。計時動畫也可以簡化,方法是在 JS 中預先計算動畫的每個影格,以縮小原生實作的規模。

NativeAnimatedModule.startAnimation({
type: 'timing',
frames: [0, 0.1, 0.2, 0.4, 0.65, ...],
animatedValueId: 1,
});

現在,這是動畫執行時發生情況的細分

  • 原生:原生動畫驅動程式使用 CADisplayLinkandroid.view.Choreographer 在每個影格上執行,並使用根據動畫曲線計算的新值更新它驅動的值。
  • 原生:計算中間值並將其傳遞給附加到原生檢視的 props 節點。
  • 原生:更新 UIViewandroid.View

如您所見,不再有 JS 執行緒,也不再有橋接器,這表示動畫速度更快!🎉🎉

我如何在我的應用程式中使用它?

對於一般動畫,答案很簡單,只需在啟動動畫時將 useNativeDriver: true 新增到動畫設定中即可。

之前

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
}).start();

之後

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

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

它也適用於 Animated.event,如果您有必須追蹤捲動位置的動畫,這非常有用,因為如果沒有原生驅動程式,由於 React Native 的非同步特性,它總是會比手勢落後一個影格。

之前

<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>

之後

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- Add this
)}
>
{content}
</Animated.ScrollView>

注意事項

並非所有您可以使用 Animated 執行的操作目前都受到原生 Animated 的支援。主要的限制是您只能動畫化非版面配置屬性,例如 transformopacity 將會運作,但 Flexbox 和位置屬性則不會。另一個限制是 Animated.event,它僅適用於直接事件,而不適用於冒泡事件。這表示它不適用於 PanResponder,但適用於 ScrollView#onScroll 等項目。

原生 Animated 也已成為 React Native 的一部分相當長一段時間,但從未記錄在文件中,因為它被認為是實驗性的。因此,如果您想要使用此功能,請確保您使用的是 React Native 的最新版本 (0.40+)。

資源

如需更多關於動畫的資訊,我建議觀看 Christopher Chedeau演講

如果您想深入了解動畫以及將動畫卸載到原生端如何改善使用者體驗,Krzysztof Magiera演講也值得一看。