はじめに
この記事は、 React Native Advent Calendar 2023 の13日目の記事です。
先日Expo SDK 49と一緒にExpo Router v2がリリースされました。
Expo SDK 49
https://blog.expo.dev/expo-sdk-49-c6d398cdf740
Announcing Expo Router v2
https://blog.expo.dev/introducing-expo-router-v2-3850fd5c3ca1
Expo Routerは Next.jsのようなファイルベースのルーティングを提供するライブラリで、ExperimentalですがtypedRoutesがサポートされていたり、React Navigationに比べて記述量が減るメリットがあります。
v2の目玉のひとつにはShared Element Transitionsのサポートがあり、この機能はreact-native-reanimatedのShared Element Transitionsのサポートを利用したもので、まだExperimentalのため本番推奨はされていませんが、今回はExpo RouterでShared Element Transitionsを利用した実装を試してみたので紹介します。
リポジトリはこちら
https://github.com/alternacrow/shared-element-transitions-with-expo-router
Shared Element Transitionsとは
Shared Element Transisionsとは、異なる画面間で共通の要素(例えば、画像やテキストなど)を持つコンポーネントが、画面遷移時にスムーズに移動または変形するアニメーションのことを指します。
遷移前の画面にあったコンポーネントが遷移先の画面のコンポーネントへ切れ目なくアニメーションすることで、ユーザーはより直感的でシームレスな体験を得ることができます。
制限について
制限として、Native Stackのみのサポートやアニメーション可能なスタイルなどがあります。
詳細は下記のリンクをご覧ください。
https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview/#limitation-and-known-issues
実装
実装は非常に簡単で、アニメーションさせたいコンポーネントをAnimated.View
もしくはAnimated.Image
に置き換え、共通のコンポーネントに同じsharedTransitionTag
を設定するだけです。
例えば、下記のような2つのページがあるとします。
// app/index.tsx
export default function ListPage() {
return (
<View>
<Text>List</Text>
{List.map((item) => {
return (
<Link key={item.id} href={`/item/${item.id}`}>
<Image source={item.image} />
</Link>
);
})}
</View>
);
}
// app/item/[itemId].tsx
export default function DetailPage() {
const { itemId } = useLocalSearchParams<{ itemId: string }>();
const item = List.find((item) => item.id === itemId);
return (
<View>
<Image source={item.image} />
<Text>{item.description}</Text>
</View>
);
}
このとき、ListPageのImage
からDetailPageのImage
へアニメーションさせたい場合は下記のように修正します。
// app/index.tsx
export default function ListPage() {
return (
<View>
<Text>List</Text>
{List.map((item) => {
return (
<Link key={item.id} href={`/item/${item.id}`}>
<Animated.Image
sharedTransitionTag={`item-${item.id}`}
source={item.image}
/>
</Link>
);
})}
</View>
);
}
// app/item/[itemId].tsx
export default function DetailPage() {
const { itemId } = useLocalSearchParams<{ itemId: string }>();
const item = List.find((item) => item.id === itemId);![Something went wrong]()
return (
<View>
<Animated.Image
sharedTransitionTag={`item-${item.id}`}
source={item.image}
/>
<Text>{item.description}</Text>
</View>
);
}
カスタムアニメーションを指定することもできます。
リポジトリの例だと、下記のようなアニメーションを行います。
iOSの方は問題なく動作していますが、Androidの方はStackのHeaderの高さを考慮したカスタムアニメーションが必要そうですね。
(カスタムアニメーションの実装については割愛)
iOS | Android |
---|---|
さいごに
触ってみての感想ですが、react-native-reanimatedのShared Element Transitions対応がExperimentalのため、公式でもアナウンスされているとおり本番採用はリスクがありますが、今後が楽しみな機能でした。
Expo RouterもTabやStackなどを使用するだけの簡易なルーティング程度なら採用の候補に上がりそうです。