fix: EachTrainInfo ActionSheetのスプリングアニメーション破綻を修正
iOS (isModal=true) でマリンライナー等の走行中列車を表示した際に
ActionSheet のスライドアップアニメーションが瞬間表示になる問題を修正。
【根本原因】
1. iOS onOpen の発火タイミング問題(最重要)
- ライブラリ内で onOpen が Modal.onShow にバインドされており、
スプリングアニメーション開始「前」に発火する
- onOpen 後に showThrew=true になると通過駅が追加されて高さが増加し
onSheetLayout が再発火 → スプリングがほぼ終点からリスタート
2. useEffect による非同期な高さ変化
- useThroughStations / useStopStationIDs / useTrainDiagramData が
useState([]) で初期化し useEffect で計算していたため
空リスト → フルリストの高さ変化が onSheetLayout をトリガーしていた
3. useAutoScroll の InteractionManager が Reanimated アニメーションを認識しない
【修正内容】
- EachTrainInfoCore: showThrew の初期値を useState(() => !!getCurrentStationData(...))
に変更し、走行中なら最初から true にして高さ変化を防ぐ
- useTrainDiagramData / useThroughStations / useStopStationIDs:
純粋計算関数を抽出し useState lazy initializer で初回レンダリング時から正確な高さを確保
- EachTrainInfo: onOpen/onClose で sheetOpened state を管理し EachTrainInfoCore に渡す
- useAutoScroll: setShowThrew 引数を削除、sheetOpened フラグでスクロールをゲート
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { EachTrainInfoCore } from "./EachTrainInfoCore";
|
||||
@@ -6,6 +6,16 @@ import { useSheetMaxHeight } from "./useSheetMaxHeight";
|
||||
export const EachTrainInfo = ({ payload }) => {
|
||||
const actionSheetRef = useRef(null);
|
||||
const maxHeight = useSheetMaxHeight();
|
||||
const [sheetOpened, setSheetOpened] = useState(false);
|
||||
|
||||
const handleOpen = () => {
|
||||
setSheetOpened(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSheetOpened(false);
|
||||
};
|
||||
|
||||
if (!payload) return <></>;
|
||||
return (
|
||||
<ActionSheet
|
||||
@@ -15,10 +25,11 @@ export const EachTrainInfo = ({ payload }) => {
|
||||
drawUnderStatusBar={false}
|
||||
isModal={Platform.OS === "ios" && !Platform.isPad}
|
||||
containerStyle={{ maxHeight }}
|
||||
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
//useBottomSafeAreaPadding={Platform.OS == "android"}
|
||||
>
|
||||
<EachTrainInfoCore {...{ actionSheetRef, ...payload }} />
|
||||
<EachTrainInfoCore {...{ actionSheetRef, sheetOpened, ...payload }} />
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
|
||||
import { useTrainMenu } from "../../stateBox/useTrainMenu";
|
||||
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
|
||||
import { useStationList } from "../../stateBox/useStationList";
|
||||
import { useCurrentTrain } from "../../stateBox/useCurrentTrain";
|
||||
import { useThemeColors } from "@/lib/theme";
|
||||
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
|
||||
import { useResponsive } from "@/lib/responsive";
|
||||
@@ -41,6 +42,7 @@ export const EachTrainInfoCore = ({
|
||||
openStationACFromEachTrainInfo,
|
||||
from,
|
||||
navigate,
|
||||
sheetOpened = false,
|
||||
}) => {
|
||||
const { stationList } = useStationList();
|
||||
const { allCustomTrainData } = useAllTrainDiagram();
|
||||
@@ -49,6 +51,7 @@ export const EachTrainInfoCore = ({
|
||||
const { setTrainInfo } = useTrainMenu();
|
||||
const { height } = useWindowDimensions();
|
||||
const { isLandscape } = useDeviceOrientationChange();
|
||||
const { getCurrentStationData } = useCurrentTrain();
|
||||
|
||||
const scrollRef = useRef<any>(null);
|
||||
// Custom hooks for data management
|
||||
@@ -72,7 +75,9 @@ export const EachTrainInfoCore = ({
|
||||
} = useExtendedStations(trainData, setTrainData);
|
||||
|
||||
// UI state
|
||||
const [showThrew, setShowThrew] = useState(false);
|
||||
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
|
||||
// ActionSheet の onSheetLayout が再発火してスプリングアニメーションが途中でリスタートするため)
|
||||
const [showThrew, setShowThrew] = useState(() => !!getCurrentStationData(data.trainNum));
|
||||
const [isJumped, setIsJumped] = useState(false);
|
||||
|
||||
// Auto scroll to current position
|
||||
@@ -82,7 +87,7 @@ export const EachTrainInfoCore = ({
|
||||
scrollRef,
|
||||
isJumped,
|
||||
setIsJumped,
|
||||
setShowThrew
|
||||
sheetOpened
|
||||
);
|
||||
|
||||
// Back button handler
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, MutableRefObject } from 'react';
|
||||
import { InteractionManager } from 'react-native';
|
||||
|
||||
|
||||
export const useAutoScroll = (
|
||||
@@ -8,31 +7,27 @@ export const useAutoScroll = (
|
||||
scrollRef: MutableRefObject<any>,
|
||||
isJumped: boolean,
|
||||
setIsJumped: (value: boolean) => void,
|
||||
setShowThrew: (value: boolean) => void
|
||||
sheetOpened: boolean = false
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (isJumped || !points?.length || !scrollRef) return;
|
||||
// ActionSheetのスプリングアニメーション完了後まで待機
|
||||
if (!sheetOpened || isJumped || !points?.length || !scrollRef) return;
|
||||
|
||||
const currentPositionIndex = points.findIndex((d) => d === true);
|
||||
if (currentPositionIndex === -1) return;
|
||||
|
||||
// ActionSheetの開閉アニメーション完了後にレイアウト変更を行う
|
||||
const handle = InteractionManager.runAfterInteractions(() => {
|
||||
setShowThrew(true);
|
||||
// 5駅以内の場合はスクロールしない
|
||||
if (currentPositionIndex < 5) {
|
||||
setIsJumped(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5駅以内の場合はスクロールしない
|
||||
if (currentPositionIndex < 5) {
|
||||
setIsJumped(true);
|
||||
return;
|
||||
}
|
||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||
const timer = setTimeout(() => {
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
setIsJumped(true);
|
||||
}, 100);
|
||||
|
||||
const scrollPosition = currentPositionIndex * 44 - 50;
|
||||
setTimeout(() => {
|
||||
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
|
||||
setIsJumped(true);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
return () => handle.cancel();
|
||||
}, [points, trainDataWithThrough, scrollRef, isJumped, setIsJumped, setShowThrew]);
|
||||
return () => clearTimeout(timer);
|
||||
}, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
|
||||
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
|
||||
data.map((item) => {
|
||||
const [stationName] = item.split(',');
|
||||
return stationList
|
||||
.map((lineStations) => lineStations.filter((s) => s.StationName === stationName))
|
||||
.reduce((acc, s) => acc.concat(s), [])
|
||||
.map((s) => s.StationNumber);
|
||||
});
|
||||
|
||||
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
|
||||
const { stationList } = useStationList();
|
||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>([]);
|
||||
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
|
||||
computeStopStationIDs(trainDataWithThrough, stationList)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const stationIDs = trainDataWithThrough.map((item) => {
|
||||
const [stationName] = item.split(',');
|
||||
|
||||
const matchingStations = stationList
|
||||
.map((lineStations) =>
|
||||
lineStations.filter((station) => station.StationName === stationName)
|
||||
)
|
||||
.reduce((acc, stations) => acc.concat(stations), [])
|
||||
.map((station) => station.StationNumber);
|
||||
|
||||
return matchingStations;
|
||||
});
|
||||
|
||||
setStopStationIDList(stationIDs);
|
||||
setStopStationIDList(computeStopStationIDs(trainDataWithThrough, stationList));
|
||||
}, [trainDataWithThrough, stationList]);
|
||||
|
||||
return stopStationIDList;
|
||||
|
||||
@@ -2,107 +2,114 @@ import { useState, useEffect } from 'react';
|
||||
import { lineListPair, stationIDPair } from '@/lib/getStationList';
|
||||
import { useStationList } from '@/stateBox/useStationList';
|
||||
|
||||
export const useThroughStations = (trainData) => {
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
const [trainDataWithThrough, setTrainDataWithThrough] = useState([]);
|
||||
const [haveThrough, setHaveThrough] = useState(false);
|
||||
const computeThroughStations = (
|
||||
trainData: string[],
|
||||
stationList: any[][],
|
||||
originalStationList: Record<string, any[]>
|
||||
): { trainDataWithThrough: string[]; haveThrough: boolean } => {
|
||||
if (!trainData.length) return { trainDataWithThrough: [], haveThrough: false };
|
||||
|
||||
useEffect(() => {
|
||||
if (!trainData.length) {
|
||||
setTrainDataWithThrough([]);
|
||||
return;
|
||||
let haveThrough = false;
|
||||
const isCancel: boolean[] = [];
|
||||
|
||||
const stopStationList = trainData.map((item, index, array) => {
|
||||
const [station, se] = item.split(',');
|
||||
const [, nextSe] = array[index + 1]?.split(',') || [];
|
||||
|
||||
if (nextSe) {
|
||||
// 運休判定ロジック:
|
||||
// 1. 両方が休系(休編、休発、休着など)→ 運休区間
|
||||
// 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行
|
||||
// 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行
|
||||
// 4. その他の休の組み合わせ → 運休区間
|
||||
const bothCanceled = se.includes('休') && nextSe.includes('休');
|
||||
const normalArrivalToSuspendStart =
|
||||
(se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発'));
|
||||
const suspendEndToNormalDeparture =
|
||||
(se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編');
|
||||
|
||||
isCancel.push(bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture);
|
||||
}
|
||||
|
||||
const isCancel = [];
|
||||
const stopStationList = trainData.map((item, index, array) => {
|
||||
const [station, se] = item.split(',');
|
||||
const [, nextSe] = array[index + 1]?.split(',') || [];
|
||||
if (se === '通編') haveThrough = true;
|
||||
|
||||
if (nextSe) {
|
||||
// 運休判定ロジック:
|
||||
// 1. 両方が休系(休編、休発、休着など)→ 運休区間
|
||||
// 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行
|
||||
// 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行
|
||||
// 4. その他の休の組み合わせ → 運休区間
|
||||
const bothCanceled = se.includes('休') && nextSe.includes('休');
|
||||
const normalArrivalToSuspendStart =
|
||||
(se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発'));
|
||||
const suspendEndToNormalDeparture =
|
||||
(se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編');
|
||||
|
||||
const isCanceled = bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture;
|
||||
isCancel.push(isCanceled);
|
||||
return stationList.map((a) => a.filter((d) => d.StationName === station));
|
||||
});
|
||||
|
||||
const allThroughStationList = stopStationList.map((firstItem, index, array) => {
|
||||
if (index === array.length - 1) return [];
|
||||
|
||||
const secondItem = array[index + 1];
|
||||
let betweenStationLine = '';
|
||||
let baseStationNumberFirst = '';
|
||||
let baseStationNumberSecond = '';
|
||||
|
||||
Object.keys(stationIDPair).forEach((lineName, lineIndex) => {
|
||||
if (!lineName) return;
|
||||
const haveFirst = firstItem[lineIndex];
|
||||
const haveSecond = secondItem[lineIndex];
|
||||
|
||||
if (haveFirst?.length && haveSecond?.length) {
|
||||
betweenStationLine = lineName;
|
||||
baseStationNumberFirst = haveFirst[0].StationNumber;
|
||||
baseStationNumberSecond = haveSecond[0].StationNumber;
|
||||
}
|
||||
|
||||
if (se === '通編') setHaveThrough(true);
|
||||
|
||||
return stationList.map((a) => a.filter((d) => d.StationName === station));
|
||||
});
|
||||
|
||||
const allThroughStationList = stopStationList.map((firstItem, index, array) => {
|
||||
if (index === array.length - 1) return [];
|
||||
if (!betweenStationLine) return [];
|
||||
|
||||
const secondItem = array[index + 1];
|
||||
let betweenStationLine = '';
|
||||
let baseStationNumberFirst = '';
|
||||
let baseStationNumberSecond = '';
|
||||
const allThroughStation: string[] = [];
|
||||
let reverse = false;
|
||||
|
||||
Object.keys(stationIDPair).forEach((lineName, lineIndex) => {
|
||||
if (!lineName) return;
|
||||
const haveFirst = firstItem[lineIndex];
|
||||
const haveSecond = secondItem[lineIndex];
|
||||
originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => {
|
||||
const throughStatus = isCancel[index] ? '通休編' : '通過';
|
||||
|
||||
if (haveFirst?.length && haveSecond?.length) {
|
||||
betweenStationLine = lineName;
|
||||
baseStationNumberFirst = haveFirst[0].StationNumber;
|
||||
baseStationNumberSecond = haveSecond[0].StationNumber;
|
||||
}
|
||||
});
|
||||
|
||||
if (!betweenStationLine) return [];
|
||||
|
||||
const allThroughStation = [];
|
||||
let reverse = false;
|
||||
|
||||
originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => {
|
||||
const throughStatus = isCancel[index] ? '通休編' : '通過';
|
||||
|
||||
if (
|
||||
station.StationNumber > baseStationNumberFirst &&
|
||||
station.StationNumber < baseStationNumberSecond
|
||||
) {
|
||||
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
|
||||
setHaveThrough(true);
|
||||
reverse = false;
|
||||
} else if (
|
||||
station.StationNumber < baseStationNumberFirst &&
|
||||
station.StationNumber > baseStationNumberSecond
|
||||
) {
|
||||
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
|
||||
setHaveThrough(true);
|
||||
reverse = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (reverse) allThroughStation.reverse();
|
||||
return allThroughStation;
|
||||
if (
|
||||
station.StationNumber > baseStationNumberFirst &&
|
||||
station.StationNumber < baseStationNumberSecond
|
||||
) {
|
||||
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
|
||||
haveThrough = true;
|
||||
reverse = false;
|
||||
} else if (
|
||||
station.StationNumber < baseStationNumberFirst &&
|
||||
station.StationNumber > baseStationNumberSecond
|
||||
) {
|
||||
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
|
||||
haveThrough = true;
|
||||
reverse = true;
|
||||
}
|
||||
});
|
||||
|
||||
let mainArray = [...trainData];
|
||||
let offset = 0;
|
||||
if (reverse) allThroughStation.reverse();
|
||||
return allThroughStation;
|
||||
});
|
||||
|
||||
trainData.forEach((_, index) => {
|
||||
offset += 1;
|
||||
const throughStations = allThroughStationList[index];
|
||||
|
||||
if (!throughStations?.length) return;
|
||||
let mainArray = [...trainData];
|
||||
let offset = 0;
|
||||
|
||||
mainArray.splice(offset, 0, ...throughStations);
|
||||
offset += throughStations.length;
|
||||
});
|
||||
trainData.forEach((_, index) => {
|
||||
offset += 1;
|
||||
const throughStations = allThroughStationList[index];
|
||||
if (!throughStations?.length) return;
|
||||
mainArray.splice(offset, 0, ...throughStations);
|
||||
offset += throughStations.length;
|
||||
});
|
||||
|
||||
setTrainDataWithThrough(mainArray);
|
||||
return { trainDataWithThrough: mainArray, haveThrough };
|
||||
};
|
||||
|
||||
export const useThroughStations = (trainData) => {
|
||||
const { originalStationList, stationList } = useStationList();
|
||||
|
||||
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
|
||||
const [state, setState] = useState(() =>
|
||||
computeThroughStations(trainData, stationList, originalStationList)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setState(computeThroughStations(trainData, stationList, originalStationList));
|
||||
}, [trainData, stationList, originalStationList]);
|
||||
|
||||
return { trainDataWithThrough, haveThrough };
|
||||
return { trainDataWithThrough: state.trainDataWithThrough, haveThrough: state.haveThrough };
|
||||
};
|
||||
|
||||
@@ -2,28 +2,34 @@ import { useState, useEffect } from 'react';
|
||||
import { useAllTrainDiagram } from '@/stateBox/useAllTrainDiagram';
|
||||
import { searchSpecialTrain } from '@/lib/eachTrainInfoCoreLib/searchSpecialTrain';
|
||||
|
||||
const parseTrainData = (trainNum: string, trainList: Record<string, string>) => {
|
||||
if (!trainNum) return { data: [], trueIDs: [] };
|
||||
const TD = trainList[trainNum];
|
||||
if (!TD) {
|
||||
const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList);
|
||||
return { data: [], trueIDs: specialTrainActualIDs || [] };
|
||||
}
|
||||
return { data: TD.split('#').filter((d) => d !== ''), trueIDs: [] };
|
||||
};
|
||||
|
||||
export const useTrainDiagramData = (trainNum) => {
|
||||
const { allTrainDiagram: trainList } = useAllTrainDiagram();
|
||||
const [trainData, setTrainData] = useState([]);
|
||||
const [trueTrainID, setTrueTrainID] = useState([]);
|
||||
const [isManuallyExtended, setIsManuallyExtended] = useState(false);
|
||||
|
||||
// 初回レンダリング時にコンテキストから同期的にデータを取得することで
|
||||
// ActionSheetのアニメーション中に高さが変わるのを防ぐ
|
||||
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);
|
||||
const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs);
|
||||
|
||||
useEffect(() => {
|
||||
if (!trainNum) return;
|
||||
|
||||
|
||||
// 手動で拡張されている場合は上書きしない
|
||||
if (isManuallyExtended) return;
|
||||
|
||||
const TD = trainList[trainNum];
|
||||
|
||||
if (!TD) {
|
||||
const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList);
|
||||
setTrueTrainID(specialTrainActualIDs || []);
|
||||
setTrainData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setTrainData(TD.split('#').filter((d) => d !== ''));
|
||||
|
||||
const { data, trueIDs } = parseTrainData(trainNum, trainList);
|
||||
setTrueTrainID(trueIDs);
|
||||
setTrainData(data);
|
||||
}, [trainNum, trainList, isManuallyExtended]);
|
||||
|
||||
const setTrainDataExtended = (data) => {
|
||||
|
||||
Reference in New Issue
Block a user