Files
jrshikoku/components/StationDiagram/ExGridView.tsx
2025-08-26 17:12:57 +00:00

177 lines
5.1 KiB
TypeScript

import { FC, useRef, useState, useCallback } from "react";
import { View, Text, ScrollView, useWindowDimensions } from "react-native";
import { ExGridViewItem } from "./ExGridViewItem";
import Animated, {
useAnimatedStyle,
useSharedValue,
runOnJS,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
export const ExGridView: FC<{
data: {
trainNumber: string;
array: string;
name: string;
type: string;
time: string;
}[];
}> = ({ data }) => {
const groupedData = {};
const groupKeys = [];
const { width } = useWindowDimensions();
data.forEach((item) => {
const hour = item.time.split(":")[0];
if (!groupedData[hour]) {
groupedData[hour] = [];
groupKeys.push(hour);
}
groupedData[hour].push(item);
});
// ドラッグ位置を保持する共有値
const widthX = useSharedValue(width);
const savedWidthX = useSharedValue(width);
const [scrollEnabled, setScrollEnabled] = useState(true);
const scrollRef = useRef<Animated.ScrollView>(null);
const [contentScrollPos, setContentScrollPos] = useState(0);
// ScrollViewの有効/無効を切り替える関数
const toggleScrollEnabled = useCallback((enabled: boolean) => {
setScrollEnabled(enabled);
}, []);
// パンジェスチャー(ドラッグ)のハンドラー
const pinchGesture = Gesture.Pinch()
.onUpdate((e) => {
const calc = savedWidthX.value * e.scale;
widthX.value = calc > width ? calc : width;
//runOnJS(scrollToRightEnd)();
})
.onEnd(() => {
savedWidthX.value = widthX.value;
});
const gesture = Gesture.Pan()
.minPointers(2) // 最低2本指
.maxPointers(2) // 最大2本指
.onTouchesDown((e) => {
if (e.numberOfTouches >= 2) runOnJS(toggleScrollEnabled)(false);
})
.onTouchesUp((e) => {
runOnJS(toggleScrollEnabled)(true);
})
.onEnd((e) => {
runOnJS(toggleScrollEnabled)(true);
});
// ジェスチャーを組み合わせる
const composed = Gesture.Simultaneous(pinchGesture, gesture);
// アニメーションスタイル
const animatedStyle = useAnimatedStyle(() => ({
width: widthX.value,
}));
return (
<GestureDetector gesture={composed}>
<Animated.ScrollView
horizontal
nestedScrollEnabled
pinchGestureEnabled={false}
scrollEnabled={scrollEnabled}
onScroll={(d) => {
setContentScrollPos(d.nativeEvent.contentOffset.x);
}}
onContentSizeChange={(d) => {
if (d < contentScrollPos) {
scrollRef.current?.scrollToEnd();
}
}}
ref={scrollRef}
contentContainerStyle={{
flexDirection: "column",
}}
>
<Animated.View
style={[
{
backgroundColor: "white",
width: width - 50,
flexDirection: "row",
},
animatedStyle,
]}
>
{Array.from({ length: 60 }, (_, i) => i + 1).map((num) => {
if (num % 5 === 0) {
return (
<Text
key={num}
style={{
flex: 1,
textAlign: "center",
borderRightWidth: 0.5,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
}}
>
{num - 5}
</Text>
);
} else return <></>;
})}
<Text
style={{
flex: 1,
textAlign: "center",
borderRightWidth: 0.5,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
width: 50
}}
>
()
</Text>
</Animated.View>
<Animated.ScrollView
style={[{ backgroundColor: "white", width: width }, animatedStyle]}
pinchGestureEnabled={false}
minimumZoomScale={0.5}
maximumZoomScale={3.0}
scrollEnabled={scrollEnabled}
stickyHeaderIndices={
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
}
>
{groupKeys.map((hour) => [
<View
style={{
backgroundColor: "white",
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: "#ccc",
}}
key={hour}
>
<Text style={{ fontSize: 15, zIndex: 1, backgroundColor: 'white', marginLeft: contentScrollPos }}>{hour}</Text>
</View>,
<View style={{ flexDirection: "row", position: "relative" }}>
{groupedData[hour].map((d, i) => (
<ExGridViewItem
key={d.trainNumber + i}
d={d}
index={i}
width={widthX}
/>
))}
</View>,
])}
</Animated.ScrollView>
</Animated.ScrollView>
</GestureDetector>
);
};