Поставьте зависимости:
npm install expo-haptics react-native-reanimated react-native-workletspnpm add expo-haptics react-native-reanimated react-native-workletsyarn add expo-haptics react-native-reanimated react-native-workletsbun add expo-haptics react-native-reanimated react-native-workletsСкопируйте эти файлы в src/shared/components/molecules/parallax-carousel/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useEffect, useMemo, useRef } from "react";
import { View, StyleSheet, Dimensions, Platform } from "react-native";
import { StatusBar } from "expo-status-bar";
import { ParallaxCarousel } from "@/shared/components/molecules/parallax-carousel";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
const _width = SCREEN_WIDTH - 50;
const _height = SCREEN_HEIGHT * 0.4;
const ITEMS: string[] = [
PHOTO_SOURCES["parallax-carousel-1"],
PHOTO_SOURCES["parallax-carousel-2"],
PHOTO_SOURCES["parallax-carousel-3"],
PHOTO_SOURCES["parallax-carousel-4"],
];
const CUSTOMIZE = [
{ key: "photos", label: "Фото", type: "images", default: ITEMS },
{ key: "intensity", label: "Параллакс", type: "slider", min: 0, max: 2, step: 0.05, default: 1 },
{ key: "height", label: "Высота", type: "slider", min: 0.2, max: 0.7, step: 0.01, default: 0.4 },
{ key: "glide", label: "Перелистывание", type: "slider", min: 150, max: 1500, step: 10, unit: "ms", default: 360 },
{ key: "pause", label: "Пауза кадра", type: "slider", min: 200, max: 3000, step: 20, unit: "ms", default: 500 },
] as const;
function findScroller(host: HTMLElement | null) {
if (!host) return null;
return (
[host, ...Array.from(host.querySelectorAll<HTMLElement>("*"))].find(
(node) => /auto|scroll/.test(getComputedStyle(node).overflowX),
) ?? null
);
}
const glides = new WeakMap<HTMLElement, number>();
function stopGlide(scroller: HTMLElement) {
const frame = glides.get(scroller);
if (frame !== undefined) cancelAnimationFrame(frame);
glides.delete(scroller);
}
function glideTo(scroller: HTMLElement, left: number, duration = 520) {
stopGlide(scroller);
const from = scroller.scrollLeft;
const start = performance.now();
const step = (now: number) => {
const progress = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - progress, 3);
scroller.scrollLeft = from + (left - from) * eased;
if (progress < 1) glides.set(scroller, requestAnimationFrame(step));
else glides.delete(scroller);
};
glides.set(scroller, requestAnimationFrame(step));
}
function useWebDragCarousel(step: number, count: number) {
const hostRef = useRef<View>(null);
useEffect(() => {
if (Platform.OS !== "web") return;
const host = hostRef.current as unknown as HTMLElement | null;
const scroller = findScroller(host);
if (!host || !scroller) return;
let startX = 0;
let startLeft = 0;
let lastX = 0;
let lastTime = 0;
let velocity = 0;
let dragging = false;
const onDown = (event: MouseEvent) => {
if (event.button !== 0) return;
event.preventDefault();
stopGlide(scroller);
dragging = true;
startX = lastX = event.clientX;
startLeft = scroller.scrollLeft;
lastTime = performance.now();
velocity = 0;
};
const onMove = (event: MouseEvent) => {
if (!dragging) return;
const now = performance.now();
velocity = (event.clientX - lastX) / Math.max(1, now - lastTime);
lastX = event.clientX;
lastTime = now;
scroller.scrollLeft = startLeft - (event.clientX - startX);
};
const onUp = () => {
if (!dragging) return;
dragging = false;
const projected = scroller.scrollLeft - velocity * 180;
const index = Math.max(0, Math.min(count - 1, Math.round(projected / step)));
glideTo(scroller, index * step);
};
host.addEventListener("mousedown", onDown);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
return () => {
host.removeEventListener("mousedown", onDown);
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
}, [step, count]);
const goTo = (index: number, duration?: number) => {
const scroller = findScroller(hostRef.current as unknown as HTMLElement | null);
if (scroller) glideTo(scroller, index * step, duration);
};
const currentIndex = () => {
const scroller = findScroller(hostRef.current as unknown as HTMLElement | null);
if (!scroller) return 0;
return Math.max(0, Math.min(count - 1, Math.round(scroller.scrollLeft / step)));
};
return { hostRef, goTo, currentIndex };
}
export default function App() {
const { hostRef, goTo, currentIndex } = useWebDragCarousel(_width, ITEMS.length);
const direction = useRef(1);
const settled = useRef(false);
const custom = useCustomize(CUSTOMIZE);
const timing = useRef({ glide: custom.glide, pause: custom.pause });
timing.current = { glide: custom.glide, pause: custom.pause };
const { photos } = custom;
const data = useMemo(() => photos.map((v) => ({ image: { uri: v } })), [photos]);
useAutoplay(async ({ wait }) => {
let index = currentIndex();
if (!settled.current) {
goTo(index, timing.current.glide);
await wait(timing.current.pause);
}
settled.current = false;
for (let stepCount = 0; stepCount < (ITEMS.length - 1) * 2; stepCount += 1) {
if (index + direction.current < 0 || index + direction.current > ITEMS.length - 1) {
direction.current = -direction.current;
}
index += direction.current;
goTo(index, timing.current.glide);
await wait(index === 0 ? timing.current.pause + 120 : timing.current.pause);
}
settled.current = true;
});
return (
<Showcase>
<View ref={hostRef} style={styles.container}>
<StatusBar style="inverted" />
<ParallaxCarousel
data={data}
renderItem={() => <></>}
itemHeight={custom.height === 0.4 ? _height : SCREEN_HEIGHT * custom.height}
itemWidth={_width}
parallaxIntensity={custom.intensity}
/>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
},
});dataТип readonly ItemT[]
renderItemТип (info: { item: ItemT; index: number }) => ReactNode
keyExtractor?Тип (item: ItemT, index: number) => string
itemWidth?Тип number
itemHeight?Тип number
spacing?Тип number
parallaxIntensity?Тип number
pagingEnabled?Тип boolean
showHorizontalScrollIndicator?Тип boolean
itemТип ItemT
indexТип number
scrollXТип SharedValue<number>
renderItemТип (info: { item: ItemT; index: number }) => ReactNode
itemWidthТип number
itemHeightТип number
spacingТип number
parallaxIntensityТип number