現在時刻が表示されたり走行位置から列車時刻を更新したりアニメーションを強化した

This commit is contained in:
harukin-expo-dev-env
2025-08-27 17:12:34 +00:00
parent 35f1860b03
commit 92d37b7277
4 changed files with 278 additions and 141 deletions

View File

@@ -1,13 +1,29 @@
import { FC, useRef, useState, useCallback } from "react";
import { View, Text, ScrollView, useWindowDimensions } from "react-native";
import {
View,
Text,
ScrollView,
useWindowDimensions,
Vibration,
} from "react-native";
import { ExGridViewItem } from "./ExGridViewItem";
import Animated, {
useAnimatedStyle,
useSharedValue,
runOnJS,
useAnimatedScrollHandler,
withTiming,
Easing,
FadeIn,
FadeOut,
BounceInUp,
FadeInUp,
FadeOutUp,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { ExGridViewTimePositionItem } from "./ExGridViewTimePositionItem";
import { useCurrentTrain } from "@/stateBox/useCurrentTrain";
import dayjs from "dayjs";
export const ExGridView: FC<{
data: {
@@ -18,21 +34,48 @@ export const ExGridView: FC<{
time: string;
}[];
}> = ({ data }) => {
const groupedData = {};
const groupKeys = [];
const groupedData: {
[d: number]: {
trainNumber: string;
array: string;
name: string;
type: string;
time: string;
isOperating: boolean;
}[];
} = {};
const groupKeys: string[] = [];
const { width } = useWindowDimensions();
const { currentTrain } = useCurrentTrain();
data.forEach((item) => {
const hour = item.time.split(":")[0];
let isOperating = false;
let [hour, minute] = item.time.split(":");
if (currentTrain.findIndex((x) => x.num == item.trainNumber) != -1) {
const currentTrainTime = currentTrain.find(
(x) => x.num == item.trainNumber
)?.delay;
if (currentTrainTime != "入線") {
[hour, minute] = dayjs()
.hour(parseInt(hour))
.minute(parseInt(minute))
.add(parseInt(currentTrainTime), "minute")
.format("H:m")
.split(":");
}
isOperating = true;
}
if (!groupedData[hour]) {
groupedData[hour] = [];
groupKeys.push(hour);
}
groupedData[hour].push(item);
groupedData[hour].push({ ...item, time: `${hour}:${minute}`, isOperating });
});
// ドラッグ位置を保持する共有値
const widthX = useSharedValue(width);
const savedWidthX = useSharedValue(width);
const isChanging = useSharedValue(false);
const animationProgress = useSharedValue(0); // アニメーション進行度
const [scrollEnabled, setScrollEnabled] = useState(true);
const scrollRef = useRef<Animated.ScrollView>(null);
@@ -55,21 +98,41 @@ export const ExGridView: FC<{
const gesture = Gesture.Pan()
.minPointers(2) // 最低2本指
.maxPointers(2) // 最大2本指
.onTouchesDown((e) => {
if (e.numberOfTouches >= 2) runOnJS(toggleScrollEnabled)(false);
.onStart(() => {
runOnJS(toggleScrollEnabled)(false);
})
.onTouchesUp((e) => {
runOnJS(toggleScrollEnabled)(true);
})
.onEnd((e) => {
.onEnd(() => {
runOnJS(toggleScrollEnabled)(true);
savedWidthX.value = widthX.value;
});
const longPressGesture = Gesture.Pan()
.minPointers(1)
.maxPointers(1)
.activateAfterLongPress(200)
.onStart(() => {
runOnJS(Vibration.vibrate)(30);
isChanging.value = true;
})
.onUpdate((e) => {
const calc = widthX.value + e.velocityY;
widthX.value = calc > width ? calc : width;
})
.onEnd(() => {
console.log("Long press ended");
isChanging.value = false;
});
// ジェスチャーを組み合わせる
const composed = Gesture.Simultaneous(pinchGesture, gesture);
const composed = Gesture.Simultaneous(
longPressGesture,
pinchGesture,
gesture
);
// アニメーションスタイル
const animatedStyle = useAnimatedStyle(() => ({
width: widthX.value,
backgroundColor: isChanging.value ? "#8adeffff" : "white",
}));
// 時ヘッダーを横にスクロールしたときの処理
const scrollX = useSharedValue(0);
@@ -81,120 +144,143 @@ export const ExGridView: FC<{
const stickyTextStyle = useAnimatedStyle(() => ({
transform: [{ translateX: scrollX.value }],
}));
const animatedLongPressStyle = useAnimatedStyle(() => ({
display: isChanging.value ? "flex" : "none",
}));
return (
<GestureDetector gesture={composed}>
<Animated.ScrollView
horizontal
nestedScrollEnabled
pinchGestureEnabled={false}
scrollEnabled={scrollEnabled}
onScroll={scrollHandler}
onContentSizeChange={(contentWidth) => {
// 現在のスクロール位置を取得
const currentScrollX = scrollX.value;
const containerWidth = width - 50;
// コンテンツが画面からはみ出している場合のみ右端にスクロール
if (currentScrollX + containerWidth > contentWidth) {
const newScrollX = Math.max(0, contentWidth - containerWidth);
scrollRef.current?.scrollTo({ x: newScrollX, animated: true });
}
}}
ref={scrollRef}
contentContainerStyle={{
flexDirection: "column",
backgroundColor: "white",
}}
<>
<Animated.View
style={[
{
position: "absolute",
width,
backgroundColor: "#26d1baff",
zIndex: 500,
top:0
},
animatedLongPressStyle,
]}
entering={FadeInUp} exiting={FadeOutUp}
>
<Animated.View
style={[
{
width: width,
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={{
textAlign: "center",
borderRightWidth: 0.5,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
width: 50,
}}
>
()
</Text>
</Animated.View>
<Text style={{ fontSize: 30, textAlign: "center", flex: 1 }}>
  
</Text>
</Animated.View>
<GestureDetector gesture={composed}>
<Animated.ScrollView
style={[{ width: width }, animatedStyle]}
horizontal
nestedScrollEnabled
pinchGestureEnabled={false}
minimumZoomScale={0.5}
maximumZoomScale={3.0}
scrollEnabled={scrollEnabled}
stickyHeaderIndices={
groupKeys.at(0) ? groupKeys.map((_, i) => i * 2) : []
}
onScroll={scrollHandler}
onContentSizeChange={(contentWidth) => {
// 現在のスクロール位置を取得
const currentScrollX = scrollX.value;
const containerWidth = width - 50;
// コンテンツが画面からはみ出している場合のみ右端にスクロール
if (currentScrollX + containerWidth > contentWidth) {
const newScrollX = Math.max(0, contentWidth - containerWidth);
scrollRef.current?.scrollTo({ x: newScrollX, animated: true });
}
}}
ref={scrollRef}
contentContainerStyle={{
flexDirection: "column",
backgroundColor: "white",
}}
>
{groupKeys.map((hour) => [
<View
<Animated.View
style={[
{
width: width,
flexDirection: "row",
},
animatedStyle,
]}
>
{Array.from({ length: 60 }, (_, i) => i + 1).map((num) => {
if (num % 5 === 0) {
return (
<Text
key={num}
style={{
flex: 1,
textAlign: "left",
borderRightWidth: 0.5,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
}}
>
{num - 5}
</Text>
);
} else return <></>;
})}
<Text
style={{
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: "#ccc",
backgroundColor: "#f0f0f0",
textAlign: "right",
borderRightWidth: 0.5,
borderColor: "#ccc",
flexWrap: "nowrap",
fontSize: 12,
width: 50,
}}
key={hour}
>
<Animated.Text
style={[
{
fontSize: 15,
zIndex: 1,
marginLeft: 0,
},
stickyTextStyle,
]}
()
</Text>
</Animated.View>
<Animated.ScrollView
style={[{ 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={{
padding: 5,
borderBottomWidth: 0.5,
borderTopWidth: 0.5,
borderBottomColor: "#ccc",
backgroundColor: "#f0f0f0",
}}
key={hour}
>
{hour}
</Animated.Text>
</View>,
<View style={{ flexDirection: "row", position: "relative" }}>
{groupedData[hour].map((d, i,array) => (
<ExGridViewItem
key={d.trainNumber + i}
d={d}
index={i}
width={widthX}
array={array}
/>
))}
</View>,
])}
<Animated.Text
style={[
{
fontSize: 15,
zIndex: 1,
marginLeft: 0,
},
stickyTextStyle,
]}
>
{hour}
</Animated.Text>
</View>,
<View style={{ flexDirection: "row", position: "relative" }}>
{groupedData[hour].map((d, i, array) => (
<ExGridViewItem
key={d.trainNumber + i}
d={d}
index={i}
width={widthX}
array={array}
/>
))}
<ExGridViewTimePositionItem width={widthX} hour={hour} />
</View>,
])}
</Animated.ScrollView>
</Animated.ScrollView>
</Animated.ScrollView>
</GestureDetector>
</GestureDetector>
</>
);
};

View File

@@ -26,6 +26,7 @@ export const ExGridViewItem: FC<{
name: string;
type: string;
time: string;
isOperating: boolean;
};
index: number;
width: SharedValue<number>;
@@ -235,7 +236,7 @@ export const ExGridViewItem: FC<{
>
<TouchableOpacity style={{ flex: 1 }} onPress={() => openTrainInfo()}>
<View style={{ position: "relative" }}>
<Text style={{ fontSize: 20, color: color, opacity: isSameTimeBefore ? 0 : 1 }}>{formattedTime}</Text>
<Text style={{ fontSize: 20, color: color, opacity: isSameTimeBefore ? 0 : 1, fontWeight:d.isOperating ? "bold" : "normal", fontStyle:d.isOperating? "italic" :"normal" }}>{formattedTime}</Text>
<Text
style={{
fontSize: 10,

View File

@@ -0,0 +1,44 @@
import { FC } from "react";
import { View } from "react-native";
import dayjs from "dayjs";
import { SharedValue, useAnimatedStyle } from "react-native-reanimated";
import Animated from "react-native-reanimated";
export const ExGridViewTimePositionItem: FC<{
width: SharedValue<number>;
hour: string;
}> = ({ width, hour }) => {
const date = dayjs();
const formattedTime = date.format("m");
const formattedHour = date.format("H");
// if(typeString == "回送"){
// return<></>;
// }
const animatedStyle = useAnimatedStyle(() => {
const leftPosition =
((((width.value - 50) / 100) * parseInt(formattedTime)) / 60) * 100;
return {
left: leftPosition,
};
}, [formattedTime]);
if (formattedHour != hour) return <></>;
return (
<View style={{ left: 0, height: 50, width: 1 }}>
<Animated.View
style={[
{
flexDirection: "column",
borderLeftWidth: 2,
//borderBottomWidth: 0.5,
borderStyle: "solid",
borderColor: "red",
position: "absolute",
height: "100%",
},
animatedStyle,
]}
/>
</View>
);
};

View File

@@ -50,29 +50,35 @@ export const StationDiagramView: FC<props> = ({ route }) => {
useEffect(() => {
if (allTrainDiagram && currentStation.length > 0) {
const stationName = currentStation[0].Station_JP;
let returnDataArray = [];
let returnDataArray: {
trainNumber: string;
array: any;
name: any;
type: any;
time: any;
}[] = [];
Object.keys(allTrainDiagram).forEach((d) => {
allTrainDiagram[d]
.split("#")
.filter((d) => {
const [station,type,time] = d.split(",");
return station === stationName;
})
.forEach((x) => {
const [name, type, time] = x.split(",");
if (!name || !type || !time) return;
const arrayData = {
trainNumber: d,
array: allTrainDiagram[d],
name,
type,
time,
};
// //条件によってフィルタリング
// if(type && type.includes("通")) return;
// if(type && type.includes("着")) return;
returnDataArray.push(arrayData);
});
allTrainDiagram[d]
.split("#")
.filter((d) => {
const [station, type, time] = d.split(",");
return station === stationName;
})
.forEach((x) => {
const [name, type, time] = x.split(",");
if (!name || !type || !time) return;
const arrayData = {
trainNumber: d,
array: allTrainDiagram[d],
name,
type,
time,
};
// //条件によってフィルタリング
// if(type && type.includes("通")) return;
// if(type && type.includes("着")) return;
returnDataArray.push(arrayData);
});
});
setCurrentStationDiagram(
returnDataArray.sort((a, b) => {
@@ -105,8 +111,8 @@ export const StationDiagramView: FC<props> = ({ route }) => {
>
{currentStation[0].Station_JP}
</Text>
{/* <ListView data={currentStationDiagram} /> */}
<ExGridView data={currentStationDiagram} />
{/* <ListView data={currentStationDiagram} /> */}
<ExGridView data={currentStationDiagram} />
{/* <Text
style={{
backgroundColor: "white",