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には、アニメーションの開始時にJavaScriptからネイティブにアニメーション定義を送信するuseNativeDriver
というフラグがあり、ネイティブ側が毎フレーム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を使用するほとんどのコンポーネントは、状態に値を格納します。
このアニメーションを、完全なアニメーションの異なる時点で行われるステップとして考えているため、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から始まり、アニメーションの終わりまでに通常のスケールまで縮小します。
そして今、コードで。
基本的に上記で行ったのは、アニメーションの進行状況の割合から個々の部分の値へのマッピングです。.interpolate
を使用してAnimatedでそれを行います。アニメーションの各部分に対して3つの異なるスタイルオブジェクトを作成し、this.state.loadingProgress
に基づいた補間された値を使用します。
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],
}),
},
],
};
これらのスタイルオブジェクトが作成されたので、投稿の前のビューのスニペットをレンダリングする際に使用できます。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に公開されており、react-native-mask-loaderとしてGitHubにあります。これを携帯電話で試すには、Expoでここにあります
- このgitbookは、React Nativeのドキュメントを読んだ後に、Animatedについてさらに学ぶための優れたリソースです。
- 実際のTwitterアニメーションは、マスクの表示を終了時に高速化しているようです。その動作に最適に一致するように、ローダーを別のイージング関数(またはスプリング!)を使用するように変更してみてください。
- マスクの現在の最終スケールはハードコードされており、タブレットではアプリ全体が表示されない可能性があります。画面サイズと画像サイズに基づいて最終スケールを計算することは、素晴らしいPRになるでしょう。