在 React Native 中實作 Twitter 的 App 載入動畫
我非常喜歡 Twitter iOS 應用程式的載入動畫。

一旦應用程式準備就緒,Twitter 標誌會愉快地展開,顯示應用程式。
我想弄清楚如何在 React Native 中重新建立這個載入動畫。
為了理解如何建置它,我首先必須理解載入動畫的不同組成部分。要看到其中的微妙之處,最簡單的方法是放慢速度。

其中有幾個主要部分,我們需要弄清楚如何建置。
- 縮放小鳥。
- 隨著小鳥變大,顯示底下的應用程式
- 在結束時稍微縮小應用程式
我花了一段時間才弄清楚如何製作這個動畫。
我一開始有個不正確的假設,認為藍色背景和 Twitter 小鳥是在應用程式上方的圖層,並且隨著小鳥變大,它會變得透明,從而顯示底下的應用程式。這種方法行不通,因為 Twitter 小鳥變得透明會顯示藍色圖層,而不是底下的應用程式!
親愛的讀者,您很幸運,不必經歷我所經歷的挫折。您可以直接跳到重點,閱讀這篇精美的教學!
正確的方法
在開始編碼之前,重要的是要了解如何分解這個動畫。為了幫助視覺化這個效果,我在 CodePen 中重新建立了它(嵌入在幾個段落中),讓您可以互動式地查看不同的圖層。

這個效果有三個主要圖層。第一個是藍色背景圖層。即使它看起來顯示在應用程式的頂部,但實際上它在最底層。
然後我們有一個純白色圖層。最後,在最前面是我們的應用程式。

這個動畫的主要技巧是使用 Twitter 標誌作為遮罩
,並遮罩應用程式和白色圖層。我不會深入探討遮罩的細節,網路上有很多相關的資源、資源和資源。
在這個情境中,遮罩的基本原理是讓遮罩的不透明像素顯示它們所遮罩的內容,而遮罩的透明像素則隱藏它們所遮罩的內容。
我們使用 Twitter 標誌作為遮罩,並讓它遮罩兩個圖層:純白色圖層和應用程式圖層。
為了顯示應用程式,我們將遮罩放大,直到它比整個螢幕還大。
在遮罩放大的同時,我們淡入應用程式圖層的不透明度,顯示應用程式並隱藏其後面的純白色圖層。為了完成效果,我們將應用程式圖層的比例設定為 > 1,並在動畫結束時將其縮小為 1。然後,我們隱藏非應用程式圖層,因為它們永遠不會再被看到。
俗話說,一張圖片勝過千言萬語。那麼,一個互動式視覺化值多少字呢?使用「下一步」按鈕點擊瀏覽動畫。顯示圖層可讓您從側面視角觀看。網格有助於視覺化透明圖層。
現在,開始 React Native
好的。既然我們知道我們要建置什麼以及動畫如何運作,我們就可以開始編碼了 — 這才是您真正來這裡的原因。
這個謎題的主要部分是 MaskedViewIOS,一個核心 React Native 組件。
import {MaskedViewIOS} from 'react-native';
<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;
MaskedViewIOS
接受 props maskElement
和 children
。children 會被 maskElement
遮罩。請注意,遮罩不必是圖片,它可以是任何任意視圖。上述範例的行為是呈現藍色視圖,但只在 maskElement
中「Basic Mask」文字所在的位置可見。我們只是製作了複雜的藍色文字。
我們想要做的是呈現我們的藍色圖層,然後在頂部呈現使用 Twitter 標誌遮罩的應用程式和白色圖層。
{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;
這將為我們提供以下圖層。

現在開始 Animated 部分
我們擁有使這個動畫運作所需的所有部分,下一步是為它們製作動畫。為了使這個動畫感覺良好,我們將使用 React Native 的 Animated API。
Animated 讓您可以在 JavaScript 中宣告式地定義動畫。預設情況下,這些動畫在 JavaScript 中執行,並告知原生圖層在每個影格上要進行哪些變更。即使 JavaScript 會嘗試在每個影格更新動畫,但它可能無法做到足夠快,並且會導致影格丟失(卡頓)發生。這不是我們想要的!
Animated 具有特殊行為,可讓您獲得沒有這種卡頓的動畫。Animated 有一個名為 useNativeDriver
的標誌,它會在動畫開始時將您的動畫定義從 JavaScript 發送到原生端,從而允許原生端處理動畫的更新,而無需在每個影格都來回 JavaScript。useNativeDriver
的缺點是您只能更新一組特定的屬性,主要是 transform
和 opacity
。您無法使用 useNativeDriver
為背景顏色等項目製作動畫,至少目前還不行 — 我們會隨著時間的推移新增更多功能,當然,您始終可以為您的專案所需的屬性提交 PR,造福整個社群 😀。
由於我們希望這個動畫流暢,我們將在這些限制範圍內工作。如需深入了解 useNativeDriver
在底層如何運作,請查看我們公告它的部落格文章。
分解我們的動畫
我們的動畫有 4 個組件
- 放大小鳥,顯示應用程式和純白色圖層
- 淡入應用程式
- 縮小應用程式
- 完成時隱藏白色圖層和藍色圖層
使用 Animated,有兩種主要方法可以定義您的動畫。第一種是使用 Animated.timing
,它讓您可以準確地說出動畫將運行多長時間,以及一個緩和曲線來平滑運動。另一種方法是使用基於物理的 API,例如 Animated.spring
。使用 Animated.spring
,您可以指定彈簧中的摩擦力和張力等參數,並讓物理運行您的動畫。
我們有多個動畫要同時運行,它們都彼此密切相關。例如,我們希望在遮罩正在顯示一半時開始淡入應用程式。由於這些動畫密切相關,我們將使用 Animated.timing
和單個 Animated.Value
。
Animated.Value
是 Animated 用於了解動畫狀態的原生值的包裝器。對於完整的動畫,您通常只想擁有其中一個。大多數使用 Animated 的組件都會將值儲存在狀態中。
由於我將這個動畫視為在完整動畫的不同時間點發生的步驟,我們將從 0 開始我們的 Animated.Value
,表示完成度為 0%,並在 100 結束我們的值,表示完成度為 100%。
我們的初始組件狀態將如下所示。
state = {
loadingProgress: new Animated.Value(0),
};
當我們準備好開始動畫時,我們告訴 Animated 將這個值動畫化為 100。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true, // This is important!
}).start();
然後,我嘗試粗略估計動畫的不同部分以及我希望它們在整個動畫的不同階段具有的值。下表列出了動畫的不同部分,以及我認為它們在我們隨著時間進展的不同點上應有的值。
Twitter 小鳥遮罩應從比例 1 開始,並且在向上射出大小之前會變小。因此,在動畫進行到 10% 時,它的比例值應為 0.8,然後在結束時向上射到比例 70。老實說,選擇 70 是相當隨意的,它需要足夠大,才能讓小鳥完全顯示螢幕,而 60 不夠大 😀。關於這部分有趣的是,數字越高,它看起來成長得越快,因為它必須在相同的時間量內到達那裡。這個數字經過一些試驗和錯誤,才能使這個標誌看起來不錯。不同尺寸的標誌/裝置將需要這個最終比例不同,以確保整個螢幕都被顯示出來。
應用程式應保持不透明一段時間,至少在 Twitter 標誌變小的過程中保持不透明。根據官方動畫,我希望在小鳥放大到一半時開始顯示它,並在很快的時間內完全顯示它。因此,在 15% 時我們開始顯示它,在整個動畫的 30% 時,它完全可見。
應用程式比例從 1.1 開始,並在動畫結束時縮小到其正常比例。
現在,開始編碼。
我們基本上在上面所做的是將動畫進度百分比的值對應到各個部分的值。我們使用 Animated 和 .interpolate
來做到這一點。我們基於 this.state.loadingProgress
的內插值,為動畫的每個部分建立 3 個不同的樣式物件。
const loadingProgress = this.state.loadingProgress;
const opacityClearToVisible = {
opacity: loadingProgress.interpolate({
inputRange: [0, 15, 30],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
// clamp means when the input is 30-100, output should stay at 1
}),
};
const imageScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 10, 100],
outputRange: [1, 0.8, 70],
}),
},
],
};
const appScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 100],
outputRange: [1.1, 1],
}),
},
],
};
現在我們有了這些樣式物件,我們可以在呈現文章前面部分的視圖程式碼片段時使用它們。請注意,只有 Animated.View
、Animated.Text
和 Animated.Image
才能使用使用 Animated.Value
的樣式物件。
const fullScreenBlueLayer = (
<View style={styles.fullScreenBlueLayer} />
);
const fullScreenWhiteLayer = (
<View style={styles.fullScreenWhiteLayer} />
);
return (
<View style={styles.fullScreen}>
{fullScreenBlueLayer}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Animated.Image
style={[styles.maskImageStyle, imageScale]}
source={twitterLogo}
/>
</View>
}>
{fullScreenWhiteLayer}
<Animated.View
style={[opacityClearToVisible, appScale, {flex: 1}]}>
{this.props.children}
</Animated.View>
</MaskedViewIOS>
</View>
);

耶!我們現在的動畫部分看起來符合我們的期望了。現在我們只需要清理我們的藍色和白色圖層,它們永遠不會再被看到。
為了知道何時可以清理它們,我們需要知道動畫何時完成。幸運的是,在我們調用 Animated.timing
的地方,.start
接受一個可選的回呼,該回呼在動畫完成時運行。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start(() => {
this.setState({
animationDone: true,
});
});
現在我們在 state
中有一個值來知道我們是否已完成動畫,我們可以修改我們的藍色和白色圖層以使用它。
const fullScreenBlueLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenBlueLayer]} />
);
const fullScreenWhiteLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenWhiteLayer]} />
);
瞧!我們的動畫現在可以運作了,並且我們在動畫完成後清理了未使用的圖層。我們已經建置了 Twitter 應用程式載入動畫!
但是等等,我的程式碼無法運作!
親愛的讀者,別擔心。我也討厭指南只提供您部分程式碼,而不提供完整的來源。
這個組件已發布到 npm,並在 GitHub 上以 react-native-mask-loader 的形式提供。若要在您的手機上試用,請在此處的 Expo 上使用

更多閱讀資源 / 額外加分
- 這本 gitbook 是在您閱讀完 React Native 文件後,了解更多關於 Animated 的絕佳資源。
- 實際的 Twitter 動畫似乎在接近尾聲時加速了遮罩顯示。嘗試修改載入器以使用不同的緩和函數(或彈簧!),以更好地匹配該行為。
- 遮罩目前的最終比例是硬式編碼的,並且可能無法在平板電腦上顯示整個應用程式。根據螢幕尺寸和圖片尺寸計算最終比例將是一個很棒的 PR。