116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
import React, { useEffect } from "react";
|
|
import { useItemContext } from "react-native-sortables";
|
|
import Animated, {
|
|
Easing,
|
|
interpolate,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withDelay,
|
|
withTiming,
|
|
} from "react-native-reanimated";
|
|
import { GridMiniSign } from "./GridMiniSign";
|
|
|
|
type Props = {
|
|
item: any;
|
|
cellW: number;
|
|
cellH: number;
|
|
startX: number;
|
|
startY: number;
|
|
exitX: number;
|
|
exitY: number;
|
|
startScale: number;
|
|
isCurrentCard: boolean;
|
|
isExiting: boolean;
|
|
exitDelay: number;
|
|
onPress?: () => void;
|
|
};
|
|
|
|
const EASE_OUT = Easing.out(Easing.cubic);
|
|
const DURATION = 320;
|
|
|
|
/** グリッドセルへのスライド+スケールアニメーション付きカード */
|
|
export const SortGridCard = React.memo(function SortGridCard({
|
|
item,
|
|
cellW,
|
|
cellH,
|
|
startX,
|
|
startY,
|
|
exitX,
|
|
exitY,
|
|
startScale,
|
|
isCurrentCard,
|
|
isExiting,
|
|
exitDelay,
|
|
onPress,
|
|
}: Props) {
|
|
const { activationAnimationProgress } = useItemContext();
|
|
|
|
// 現在選択中のカードはカルーセル位置から、それ以外は近距離からスライド
|
|
const initX = isCurrentCard ? startX : startX * 0.35;
|
|
const initY = isCurrentCard ? startY : startY * 0.35;
|
|
const initScale = isCurrentCard ? Math.min(startScale, 1.5) : Math.min(startScale, 1.15);
|
|
|
|
const tx = useSharedValue(initX);
|
|
const ty = useSharedValue(initY);
|
|
const sc = useSharedValue(initScale);
|
|
const opacity = useSharedValue(0);
|
|
|
|
// 入場
|
|
useEffect(() => {
|
|
const cfg = { duration: DURATION, easing: EASE_OUT };
|
|
tx.value = withTiming(0, cfg);
|
|
ty.value = withTiming(0, cfg);
|
|
sc.value = withTiming(1, cfg);
|
|
opacity.value = withTiming(1, { duration: 180, easing: EASE_OUT });
|
|
}, []);
|
|
|
|
// 退場
|
|
useEffect(() => {
|
|
if (!isExiting) return;
|
|
const cfg = { duration: DURATION, easing: EASE_OUT };
|
|
const toX = isCurrentCard ? exitX : exitX * 0.35;
|
|
const toY = isCurrentCard ? exitY : exitY * 0.35;
|
|
tx.value = withDelay(exitDelay, withTiming(toX, cfg));
|
|
ty.value = withDelay(exitDelay, withTiming(toY, cfg));
|
|
sc.value = withDelay(exitDelay, withTiming(initScale, cfg));
|
|
opacity.value = withDelay(exitDelay, withTiming(0, { duration: 150, easing: EASE_OUT }));
|
|
}, [isExiting]);
|
|
|
|
const animStyle = useAnimatedStyle(() => {
|
|
const p = activationAnimationProgress.value;
|
|
return {
|
|
opacity: opacity.value * interpolate(p, [0, 1], [1, 0.85]),
|
|
shadowOpacity: interpolate(p, [0, 1], [0, 0.4]),
|
|
shadowRadius: interpolate(p, [0, 1], [0, 10]),
|
|
elevation: interpolate(p, [0, 1], [1, 12]),
|
|
transform: [
|
|
{ translateX: tx.value },
|
|
{ translateY: ty.value },
|
|
{ scale: sc.value * interpolate(p, [0, 1], [1, 1.06]) },
|
|
] as any,
|
|
};
|
|
});
|
|
|
|
return (
|
|
<Animated.View
|
|
style={[
|
|
{
|
|
width: cellW,
|
|
height: cellH,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 4 },
|
|
},
|
|
animStyle,
|
|
]}
|
|
>
|
|
<GridMiniSign
|
|
item={item}
|
|
width={cellW}
|
|
height={cellH}
|
|
onPress={onPress}
|
|
/>
|
|
</Animated.View>
|
|
);
|
|
});
|
|
|