Files
harukin-expo-dev-env f210ca4d77 feat: Add loading states and map functionality to the menu
- Implemented StationListLoadingState component for loading messages.
- Created MenuLED component to display LED information with loading animations.
- Developed MenuMap component for interactive map features, including user location markers and station markers.
- Introduced LEDFrame component for consistent styling of LED displays.
- Enhanced LED_vision component to support embedded rendering and content readiness.
- Added menuStationSelectors for managing nearby and favorite station groups.
- Updated Menu component to integrate new loading states and map functionalities.
- Added tests for menuStationSelectors to ensure correct grouping and selection logic.
2026-09-02 21:24:41 +09:00

213 lines
5.7 KiB
TypeScript

import React, { useEffect, useRef } from "react";
import { Image, LayoutAnimation, Platform, View } from "react-native";
import type { LocationObject } from "expo-location";
import MapView, { Marker } from "react-native-maps";
import type { StationProps } from "@/lib/CommonTypes";
type MenuMapProps = {
mapHeight: number;
mapFullHeight: number;
mapMode: boolean;
backgroundColor: string;
isDark: boolean;
isHeavyContentReady: boolean;
listIndex: number;
listUpStation: StationProps[][];
position: LocationObject | undefined;
mapPaddingTop: number;
mapPaddingBottom: number;
setMapMode: React.Dispatch<React.SetStateAction<boolean>>;
setListIndex: (index: number) => void;
onGoToMap: () => void;
onReturnToTop: () => void;
};
const MAP_PIN_SIZE = Platform.OS === "android" ? 32 : 36;
const ANDROID_USER_LOCATION_MARKER_SIZE = 18;
const ANDROID_USER_LOCATION_INNER_SIZE = 8;
const configureMapLayoutAnimation = () => {
LayoutAnimation.configureNext({
duration: 300,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
});
};
const MenuMapComponent = ({
mapHeight,
mapFullHeight,
mapMode,
backgroundColor,
isDark,
isHeavyContentReady,
listIndex,
listUpStation,
position,
mapPaddingTop,
mapPaddingBottom,
setMapMode,
setListIndex,
onGoToMap,
onReturnToTop,
}: MenuMapProps) => {
const mapsRef = useRef(null);
useEffect(() => {
if (!isHeavyContentReady) return;
if (!mapsRef.current) return;
if (listUpStation.length === 0) return;
if (listUpStation[listIndex] == undefined) return;
const { lat, lng } = listUpStation[listIndex][0];
const mapRegion = {
latitude: lat,
longitude: lng,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
};
if (mapMode) {
mapsRef.current.fitToCoordinates(
listUpStation.map((stationGroup) => ({
latitude: stationGroup[0].lat,
longitude: stationGroup[0].lng,
})),
{
edgePadding: {
top: mapPaddingTop,
bottom: mapPaddingBottom,
left: 50,
right: 50,
},
},
);
} else {
mapsRef.current.animateToRegion(mapRegion, 1000);
}
// mapMode と padding の変更時には、既存挙動どおり個別追従しない
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHeavyContentReady, listIndex, listUpStation]);
const handleMapTouch = () => {
configureMapLayoutAnimation();
setMapMode(true);
onGoToMap();
};
const handleMarkerPress = (
station: { lat: number; lng: number },
index: number,
) => {
setMapMode(false);
setListIndex(index);
if (mapsRef.current) {
mapsRef.current.animateToRegion(
{
latitude: station.lat,
longitude: station.lng,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
},
1000,
);
}
configureMapLayoutAnimation();
onReturnToTop();
};
if (!isHeavyContentReady) {
return (
<View
style={{
width: "100%",
height: mapMode ? mapFullHeight : mapHeight,
backgroundColor,
}}
/>
);
}
return (
<MapView
ref={mapsRef}
style={{ width: "100%", height: mapMode ? mapFullHeight : mapHeight }}
showsUserLocation={Platform.OS !== "android"}
loadingEnabled={true}
showsMyLocationButton={false}
moveOnMarkerPress={false}
showsCompass={false}
userInterfaceStyle={isDark ? "dark" : "light"}
//provider={PROVIDER_GOOGLE}
initialRegion={{
latitude: 33.774519,
longitude: 133.533306,
latitudeDelta: 1.8,
longitudeDelta: 1.8,
}}
onTouchStart={handleMapTouch}
>
{Platform.OS === "android" && position ? (
<Marker
key="android-user-location"
coordinate={{
latitude: position.coords.latitude,
longitude: position.coords.longitude,
}}
anchor={{ x: 0.5, y: 0.5 }}
tracksViewChanges={false}
zIndex={999}
>
<View
style={{
width: ANDROID_USER_LOCATION_MARKER_SIZE,
height: ANDROID_USER_LOCATION_MARKER_SIZE,
borderRadius: ANDROID_USER_LOCATION_MARKER_SIZE / 2,
backgroundColor: "rgba(10, 132, 255, 0.24)",
borderWidth: 1,
borderColor: "rgba(10, 132, 255, 0.42)",
alignItems: "center",
justifyContent: "center",
}}
>
<View
style={{
width: ANDROID_USER_LOCATION_INNER_SIZE,
height: ANDROID_USER_LOCATION_INNER_SIZE,
borderRadius: ANDROID_USER_LOCATION_INNER_SIZE / 2,
backgroundColor: "#0A84FF",
borderWidth: 1,
borderColor: "#ffffff",
}}
/>
</View>
</Marker>
) : null}
{listUpStation.map(([{ lat, lng, StationNumber }], index) => (
<Marker
key={index + StationNumber}
coordinate={{ latitude: lat, longitude: lng }}
anchor={{ x: 0.5, y: 1 }}
tracksViewChanges={false}
onPress={() => handleMarkerPress({ lat, lng }, index)}
>
<Image
source={require("@/assets/reccha-small.png")}
style={{ width: MAP_PIN_SIZE, height: MAP_PIN_SIZE }}
resizeMode="contain"
/>
</Marker>
))}
</MapView>
);
};
export const MenuMap = React.memo(MenuMapComponent);
MenuMap.displayName = "MenuMap";