現在時刻が表示されたり走行位置から列車時刻を更新したりアニメーションを強化した
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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,
|
||||
|
44
components/StationDiagram/ExGridViewTimePositionItem.tsx
Normal file
44
components/StationDiagram/ExGridViewTimePositionItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user