Files
jrshikoku/docs/actionsheet-animation-fix.md

5.3 KiB
Raw Permalink Blame History

EachTrainInfo ActionSheet アニメーション破綻の修正記録

日付: 2026-04-08
ブランチ: fix/April-Mid-Patch
コミット: 8b42644


症状

iOS (isModal=true) でマリンライナー等の走行中の列車を EachTrainInfo ActionSheet で表示したとき、スライドアップアニメーションが瞬間表示になる。
Android では発生しない。


根本原因3層

1. iOS onOpen の発火タイミング(最重要)

ライブラリ node_modules/react-native-actions-sheet/dist/src/index.js line 962:

onShow: props.onOpen,

iOS の isModal=true モードでは ActionSheet の onOpen prop が React Native の Modal.onShow にバインドされる。
Modal.onShow はスプリングアニメーション開始前(モーダルが表示された直後)に発火する。

これにより以下の連鎖が起きていた:

  1. Modal 表示 → onOpensetShowThrew(true) 実行
  2. 通過駅が一気に追加されてシート高さが増加
  3. onSheetLayout が再発火してスプリングが「ほぼ終点位置」からリスタート
  4. 結果:スライドアップに見えず瞬間表示になる

2. useEffect による非同期な高さ変化

以下の hooks が useState([]) (空) で初期化し useEffect で計算していた:

  • useTrainDiagramData — 駅リスト本体
  • useThroughStations — 通過駅挿入後のリスト(実際にレンダリングされる)
  • useStopStationIDs — 駅ID対応表

初回レンダリング(空・高さ小)→ useEffect 完了(フルリスト・高さ大)という変化が onSheetLayout を再トリガーしていた。

3. useAutoScrollInteractionManager が非効果的だった

InteractionManager.runAfterInteractions() は JS スレッドのインタラクション完了を待つが、ActionSheet の Reanimated スプリングUI スレッド)完了は認識しないため、アニメーション途中でスクロールが実行されることがあった。


修正内容

showThrew の初期値を同期的に決定 ← 最重要

// EachTrainInfoCore.tsx修正前
const [showThrew, setShowThrew] = useState(false);

// EachTrainInfoCore.tsx修正後
const [showThrew, setShowThrew] = useState(() => !!getCurrentStationData(data.trainNum));

走行中の列車は最初から true にすることで、アニメーション中に通過駅の追加による高さ変化が起きなくなる。

各 hooks の lazy initializer 化

純粋計算関数を抽出して useState の初期化関数に渡すことで、初回レンダリング時から正確な高さを確保:

// useThroughStations.ts
const [state, setState] = useState(() =>
  computeThroughStations(trainData, stationList, originalStationList)
);

// useStopStationIDs.ts
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
  computeStopStationIDs(trainDataWithThrough, stationList)
);

// useTrainDiagramData.ts
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);

sheetOpened フラグによるスクロールのゲート

// EachTrainInfo.tsx
const [sheetOpened, setSheetOpened] = useState(false);
// onOpen → setSheetOpened(true), onClose → setSheetOpened(false)
// useAutoScroll.ts — sheetOpened が true になるまでスクロールしない
if (!sheetOpened || isJumped || ...) return;

useAutoScroll から setShowThrew 呼び出しを除去

スクロール位置制御と通過駅表示の責務を分離。setShowThrewEachTrainInfoCore の初期化時のみで完結。


変更ファイル一覧

ファイル 変更内容
components/ActionSheetComponents/EachTrainInfo.tsx sheetOpened state 追加、onOpen/onClose ハンドラ実装
components/ActionSheetComponents/EachTrainInfoCore.tsx showThrew 同期初期化、useCurrentTrain import 追加、setShowThrewuseAutoScroll から除去
components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts setShowThrew 引数削除、sheetOpened ゲート追加、InteractionManager 廃止
components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts parseTrainData 純粋関数抽出、lazy initializer 化
components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts computeThroughStations 純粋関数抽出、lazy initializer 化
components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts computeStopStationIDs 純粋関数抽出、lazy initializer 化

将来の注意点

  • ActionSheet に渡すコンテンツの高さはマウント時から固定すること。 useEffect で後から高さを変えると onSheetLayout が再発火してスプリングアニメーションがリスタートする。
  • iOS で isModal=true の場合、onOpen はアニメーション完了前に発火する。 onOpen の中で state 変更を行うとアニメーションが破綻する可能性がある。
  • Reanimated スプリングは UIスレッドで動くため InteractionManager.runAfterInteractions() では待てない。 代わりに onOpen フラグでゲートする。