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
。子元件會被 maskElement
遮罩。請注意,遮罩不需要是影像,它可以是任何任意視圖。上述範例的行為是呈現藍色視圖,但僅在 maskElement
中的「Basic Mask」文字所在的位置可見。我們只是製作了複雜的藍色文字。
我們要做的Render是藍色圖層,然後在頂部 Render 使用 Twitter 標誌遮罩的應用程式和白色圖層。
{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;
這將為我們提供以下看到的圖層。
現在是動畫部分
我們擁有使此工作運作所需的所有部分,下一步是為它們製作動畫。為了使此動畫感覺良好,我們將使用 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.Value
的 Animated.timing
。
Animated.Value
是 Animated 用於了解動畫狀態的原生值的包裝器。對於完整的動畫,您通常只想擁有其中一個。大多數使用 Animated 的元件都會將值儲存在狀態中。
由於我將此動畫視為在完整動畫的不同時間點發生的步驟,因此我們的 Animated.Value
將從 0 開始,表示完成 0%,並將值結束於 100,表示完成 100%。
我們的初始元件狀態將如下所示。
state = {
loadingProgress: new Animated.Value(0),
};
當我們準備好開始動畫時,我們告訴 Animated 將此值動畫化為 100。
Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).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',
}),
};
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],
}),
},
],
};
現在我們有了這些樣式物件,我們可以在 Render 先前文章中的視圖片段時使用它們。請注意,只有 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。