React NativeでTwitterのアプリ読み込みアニメーションを実装する
TwitterのiOSアプリには、私がとても気に入っている読み込みアニメーションがあります。

アプリの準備ができると、Twitterのロゴが楽しく展開し、アプリが表示されます。
React Nativeでこの読み込みアニメーションを再現する方法を見つけたいと思いました。
どのように構築するかを理解するために、最初に読み込みアニメーションのさまざまな部分を理解する必要がありました。微妙な違いを見る最も簡単な方法は、速度を落とすことです。

構築方法を理解する必要がある主要な部分がいくつかあります。
- 鳥の拡大縮小。
- 鳥が成長するにつれて、下にアプリを表示する
- 最後にアプリを少し縮小する
このアニメーションを作成する方法を見つけるのにかなり時間がかかりました。
青い背景とTwitterの鳥がアプリの上にあるレイヤーであり、鳥が成長するにつれて透明になり、下にアプリが表示されると誤って思い込んでいました。このアプローチは、Twitterの鳥が透明になると、下のアプリではなく青いレイヤーが表示されるため、機能しません。
幸運なことに、読者の皆様は、私が経験したのと同じフラストレーションを経験する必要はありません。この素晴らしいチュートリアルで、良い部分にスキップできます。
正しい方法
コードに移る前に、これをどのように分解するかを理解することが重要です。この効果を視覚化するために、CodePen(数段落下に埋め込み)で再現したので、さまざまなレイヤーをインタラクティブに確認できます。

この効果には、主に3つのレイヤーがあります。1つ目は青い背景レイヤーです。これはアプリの上に表示されているように見えますが、実際には後ろにあります。
次に、プレーンな白いレイヤーがあります。そして最後に、一番前にアプリがあります。

このアニメーションの主な秘訣は、Twitterのロゴをmask
として使用し、アプリと白いレイヤーの両方をマスクすることです。マスキングの詳細はここでは深く掘り下げませんが、多数の リソースがオンラインで入手できます。
このコンテキストでのマスキングの基本は、マスクの不透明なピクセルはマスクしているコンテンツを表示し、マスクの透明なピクセルはマスクしているコンテンツを非表示にする画像を持つことです。
Twitterのロゴをマスクとして使用し、2つのレイヤー(ソリッドホワイトレイヤーとアプリレイヤー)をマスクします。
アプリを表示するには、マスクを画面全体よりも大きくなるまで拡大します。
マスクが拡大されている間、アプリレイヤーの不透明度をフェードインして、アプリを表示し、その背後にあるソリッドホワイトレイヤーを非表示にします。効果を仕上げるために、アプリレイヤーを1より大きいスケールで開始し、アニメーションの終了時に1に縮小します。その後、アプリ以外のレイヤーは二度と表示されないため、非表示にします。
百聞は一見に如かずと言います。インタラクティブな視覚化はどれだけの価値があるのでしょうか?「次のステップ」ボタンをクリックしてアニメーションを進めてください。レイヤーを表示すると、側面図が表示されます。グリッドは、透明なレイヤーを視覚化するのに役立ちます。
それでは、React Nativeについて
さて、何を構築し、アニメーションがどのように機能するかを理解したので、コードに取り組むことができます。これがあなたが本当にここにいる理由です。
このパズルの主要な部分は、React NativeのコアコンポーネントであるMaskedViewIOSです。
import {MaskedViewIOS} from 'react-native';
<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;
MaskedViewIOS
は、小道具maskElement
と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>;
これにより、以下に示すレイヤーが得られます。

それでは、アニメーション化された部分について
このアニメーションを機能させるために必要なすべてのピースが揃ったので、次のステップはそれらをアニメーション化することです。このアニメーションを良い感じにするために、React NativeのAnimated APIを利用します。
Animatedを使用すると、JavaScriptでアニメーションを宣言的に定義できます。デフォルトでは、これらのアニメーションはJavaScriptで実行され、ネイティブレイヤーにフレームごとにどのような変更を加えるかを指示します。JavaScriptはフレームごとにアニメーションを更新しようとしますが、十分な速度で実行できない可能性があり、フレームのドロップ(ジャンク)が発生します。これは私たちが望むものではありません。
Animatedには、このジャンクなしでアニメーションを取得できる特別な動作があります。AnimatedにはuseNativeDriver
と呼ばれるフラグがあり、アニメーションの開始時にJavaScriptからネイティブにアニメーション定義を送信することで、ネイティブ側がJavaScriptとフレームごとにやり取りすることなくアニメーションの更新を処理できます。useNativeDriver
の欠点は、更新できるプロパティが特定のセットに限定されていることです。主にtransform
とopacity
です。少なくとも今のところ、useNativeDriver
で背景色などをアニメーション化することはできません。今後、さらに多くのプロパティを追加していく予定であり、もちろん、プロジェクトに必要なプロパティのPRを送信して、コミュニティ全体に貢献することもできます😀。
このアニメーションをスムーズにしたいので、これらの制約内で作業します。useNativeDriver
が内部でどのように機能するかについての詳細は、それを発表したブログ投稿をご覧ください。
アニメーションの分解
アニメーションには4つのコンポーネントがあります
- 鳥を拡大して、アプリとソリッドホワイトレイヤーを表示する
- アプリをフェードインする
- アプリを縮小する
- 完了したら、白いレイヤーと青いレイヤーを非表示にする
Animatedでは、アニメーションを定義する主な方法が2つあります。1つ目はAnimated.timing
を使用する方法で、アニメーションの実行時間とモーションをスムーズにするためのイージングカーブを正確に指定できます。もう1つのアプローチは、Animated.spring
などの物理ベースのAPIを使用することです。Animated.spring
では、ばねの摩擦や張力などのパラメータを指定し、物理学によってアニメーションを実行させます。
互いに密接に関連する複数のアニメーションを同時に実行したいと考えています。たとえば、マスクが表示されている間にアプリのフェードインを開始したいと考えています。これらのアニメーションは密接に関連しているため、単一の `Animated.Value` を使用した `Animated.timing` を使用します。
`Animated.Value` は、Animated がアニメーションの状態を知るために使用するネイティブ値のラッパーです。通常、完全なアニメーションにはこのうち1つだけが必要です。Animated を使用するほとんどのコンポーネントは、この値を state に格納します。
このアニメーションは、アニメーション全体を通して異なる時点で発生するステップとして考えているため、`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, // 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.Value` を使用するスタイルオブジェクトを使用できるのは、`Animated.View`、`Animated.Text`、および `Animated.Image` のみであることに注意してください。
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 になります。