Поставьте зависимости:
npm install react-native-reanimatedpnpm add react-native-reanimatedyarn add react-native-reanimatedbun add react-native-reanimatedСкопируйте эти файлы в src/shared/components/molecules/material-carousel/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useEffect, useRef } from "react";
import { View, StyleSheet, Platform } from "react-native";
import { StatusBar } from "expo-status-bar";
import MaterialCarousel from "@/shared/components/molecules/material-carousel";
import { SMALL_IMAGE } from "@/shared/components/molecules/material-carousel/const";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const ITEMS: string[] = [
PHOTO_SOURCES["material-carousel-1"],
PHOTO_SOURCES["material-carousel-2"],
PHOTO_SOURCES["material-carousel-3"],
PHOTO_SOURCES["material-carousel-4"],
];
const CUSTOMIZE = [
{ key: "photos", label: "Изображения", type: "images", default: ITEMS },
{ key: "imageHeight", label: "Высота", type: "slider", min: 200, max: 560, step: 10, unit: "px", default: 400 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 60, step: 1, unit: "px", default: 24 },
{ key: "scaleEnabled", label: "Масштаб краёв", type: "toggle", default: false },
{ key: "glideSpeed", label: "Скорость показа", type: "slider", min: 0.25, max: 3, step: 0.05, default: 1 },
] 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
);
}
function nativeScrollTo(node: HTMLElement, options: ScrollToOptions) {
Reflect.apply(Element.prototype.scrollTo, node, [options]);
}
function useWebDragCarousel(step: 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();
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 maxLeft = scroller.scrollWidth - scroller.clientWidth;
const projected = scroller.scrollLeft - velocity * 180;
const lastSnap = Math.floor(maxLeft / step) * step;
const left = Math.max(0, Math.min(lastSnap, Math.round(projected / step) * step));
nativeScrollTo(scroller, { left, behavior: "smooth" });
};
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]);
const glide = (index: number, duration: number, isCancelled: () => boolean) =>
new Promise<void>((resolve) => {
const scroller = findScroller(hostRef.current as unknown as HTMLElement | null);
if (!scroller) {
resolve();
return;
}
const from = scroller.scrollLeft;
const maxLeft = scroller.scrollWidth - scroller.clientWidth;
const to = Math.max(0, Math.min(maxLeft, index * step));
if (Math.abs(to - from) < 1) {
resolve();
return;
}
const started = performance.now();
const frame = (now: number) => {
if (isCancelled()) {
resolve();
return;
}
const progress = Math.min(1, (now - started) / duration);
const eased =
progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
scroller.scrollLeft = from + (to - from) * eased;
if (progress < 1) requestAnimationFrame(frame);
else resolve();
};
requestAnimationFrame(frame);
});
return { hostRef, glide };
}
export default function App() {
const { hostRef, glide } = useWebDragCarousel(SMALL_IMAGE);
const custom = useCustomize(CUSTOMIZE);
const glideSpeed = useRef<number>(custom.glideSpeed);
glideSpeed.current = custom.glideSpeed;
useAutoplay(async ({ wait, isCancelled }) => {
await glide(0, 500 / glideSpeed.current, isCancelled);
await wait(250);
await glide(ITEMS.length - 1, 750 / glideSpeed.current, isCancelled);
await wait(350);
await glide(0, 1150 / glideSpeed.current, isCancelled);
await wait(450);
});
return (
<Showcase>
<View style={styles.container}>
<StatusBar style="inverted" />
<View ref={hostRef}>
<MaterialCarousel
data={custom.photos}
renderItem={() => <></>}
scaleEnabled={custom.scaleEnabled}
imageHeight={custom.imageHeight}
borderRadius={custom.radius}
/>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
justifyContent: "center",
alignItems: "center",
},
});dataТип string[]
renderItemТип (item: string, index: number) => React.ReactNode
scaleEnabled?Тип boolean
imageHeight?Тип number
borderRadius?Тип number
itemТип string
scrollXТип SharedValue<number>
indexТип number
scaleEnabledТип boolean
renderItem?Тип (item: string, index: number) => React.ReactNode
dataLength?Тип number
imageHeight?Тип number
borderRadius?Тип number