Expo Routerでreact-native-reanimatedのShared Element Transitionsを使う

# はじめに

この記事は、 [React Native Advent Calendar 2023](https://qiita.com/advent-calendar/2023/react-native) の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つのページがあるとします。

```react
// 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>
  );
}
```

```react
// 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`へアニメーションさせたい場合は下記のように修正します。

```react
// 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>
  );
}
```

```react
// 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>
  );
}
```

カスタムアニメーションを指定することもできます。

https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview/#custom-animation

リポジトリの例だと、下記のようなアニメーションを行います。
iOSの方は問題なく動作していますが、Androidの方はStackのHeaderの高さを考慮したカスタムアニメーションが必要そうですね。
（カスタムアニメーションの実装については割愛）

|iOS|Android|
|:-:|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684679/72082bbe-efdd-8670-66c2-7c4ded54e361.gif" width="300" />|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684679/f04e1259-2f6a-c815-2adb-c783b372f476.gif" width="300" />|

# さいごに

触ってみての感想ですが、react-native-reanimatedのShared Element Transitions対応がExperimentalのため、公式でもアナウンスされているとおり本番採用はリスクがありますが、今後が楽しみな機能でした。
Expo RouterもTabやStackなどを使用するだけの簡易なルーティング程度なら採用の候補に上がりそうです。