5.3 KiB
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 はスプリングアニメーション開始前(モーダルが表示された直後)に発火する。
これにより以下の連鎖が起きていた:
- Modal 表示 →
onOpen→setShowThrew(true)実行 - 通過駅が一気に追加されてシート高さが増加
onSheetLayoutが再発火してスプリングが「ほぼ終点位置」からリスタート- 結果:スライドアップに見えず瞬間表示になる
2. useEffect による非同期な高さ変化
以下の hooks が useState([]) (空) で初期化し useEffect で計算していた:
useTrainDiagramData— 駅リスト本体useThroughStations— 通過駅挿入後のリスト(実際にレンダリングされる)useStopStationIDs— 駅ID対応表
初回レンダリング(空・高さ小)→ useEffect 完了(フルリスト・高さ大)という変化が onSheetLayout を再トリガーしていた。
3. useAutoScroll の InteractionManager が非効果的だった
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 呼び出しを除去
スクロール位置制御と通過駅表示の責務を分離。setShowThrew は EachTrainInfoCore の初期化時のみで完結。
変更ファイル一覧
| ファイル | 変更内容 |
|---|---|
components/ActionSheetComponents/EachTrainInfo.tsx |
sheetOpened state 追加、onOpen/onClose ハンドラ実装 |
components/ActionSheetComponents/EachTrainInfoCore.tsx |
showThrew 同期初期化、useCurrentTrain import 追加、setShowThrew を useAutoScroll から除去 |
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フラグでゲートする。