diff --git a/components/ActionSheetComponents/EachTrainInfo.tsx b/components/ActionSheetComponents/EachTrainInfo.tsx index 416f966..8aa50da 100644 --- a/components/ActionSheetComponents/EachTrainInfo.tsx +++ b/components/ActionSheetComponents/EachTrainInfo.tsx @@ -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 ( { drawUnderStatusBar={false} isModal={Platform.OS === "ios" && !Platform.isPad} containerStyle={{ maxHeight }} - + onOpen={handleOpen} + onClose={handleClose} //useBottomSafeAreaPadding={Platform.OS == "android"} > - + ); }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore.tsx b/components/ActionSheetComponents/EachTrainInfoCore.tsx index 721b572..4e37c4f 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore.tsx +++ b/components/ActionSheetComponents/EachTrainInfoCore.tsx @@ -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(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 diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts index e719341..5374558 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useAutoScroll.ts @@ -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, 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]); }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts index 30b4647..468a42f 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useStopStationIDs.ts @@ -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([]); + + // 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ + const [stopStationIDList, setStopStationIDList] = useState(() => + 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; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts index aed14c3..8fef145 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useThroughStations.ts @@ -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 +): { 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 }; }; diff --git a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts index 9e098cb..f94293a 100644 --- a/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts +++ b/components/ActionSheetComponents/EachTrainInfoCore/hooks/useTrainDiagramData.ts @@ -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) => { + 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) => {