Поставьте зависимости:
npm install @sbaiahmed1/react-native-blur react-native-reanimatedpnpm add @sbaiahmed1/react-native-blur react-native-reanimatedyarn add @sbaiahmed1/react-native-blur react-native-reanimatedbun add @sbaiahmed1/react-native-blur react-native-reanimatedСкопируйте эти файлы в src/shared/components/molecules/rotate-carousel/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useEffect, useRef } from "react";
import { View, StyleSheet, Image, Dimensions, Platform } from "react-native";
import { StatusBar } from "expo-status-bar";
import { RotateCarousel } from "@/shared/components/molecules/rotate-carousel";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const POSTERS: string[] = [
PHOTO_SOURCES["rotate-carousel-1"],
PHOTO_SOURCES["rotate-carousel-2"],
PHOTO_SOURCES["rotate-carousel-3"],
PHOTO_SOURCES["rotate-carousel-4"],
];
const ITEM_WIDTH = Dimensions.get("window").width * 0.7;
const CUSTOMIZE = [
{ key: "photos", label: "Изображения", type: "images", default: POSTERS },
{ key: "rotatePercentage", label: "Сила поворота", type: "slider", min: 0, max: 300, step: 5, unit: "%", default: 150 },
{ key: "posterWidth", label: "Ширина постера", type: "slider", min: 150, max: 300, step: 2, unit: "px", default: 250 },
{ key: "posterHeight", label: "Высота постера", type: "slider", min: 200, max: 480, step: 2, unit: "px", default: 390 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 40, step: 1, unit: "px", default: 16 },
{ key: "background", label: "Фон", type: "color", default: "#0a0a0a" },
] as const;
const TOUR = [
{ hold: 400, index: 1 },
{ hold: 460, index: 2 },
{ hold: 460, index: 3 },
{ hold: 450, index: 1 },
{ hold: 450, index: 0 },
{ hold: 700, index: 0 },
];
function findScroller(host: HTMLElement | null) {
if (!host) return null;
return (
Array.from(host.querySelectorAll<HTMLElement>("*")).find(
(node) =>
/auto|scroll/.test(getComputedStyle(node).overflowX) &&
node.scrollWidth > node.clientWidth,
) ?? null
);
}
function scrollToIndex(host: HTMLElement | null, index: number) {
const scroller = findScroller(host);
if (!scroller) return;
Element.prototype.scrollTo.call(scroller, {
left: index * ITEM_WIDTH,
behavior: "smooth",
});
}
function useWebDragScroll() {
const hostRef = useRef<View>(null);
useEffect(() => {
if (Platform.OS !== "web") return;
const host = hostRef.current as unknown as HTMLElement | null;
if (!host) return;
let scroller: HTMLElement | null = null;
let startX = 0;
let startLeft = 0;
let lastX = 0;
let lastTime = 0;
let velocity = 0;
let snap = "";
const holdSnap = (event: Event) => event.stopImmediatePropagation();
const onDown = (event: MouseEvent) => {
if (event.button !== 0) return;
scroller = findScroller(host);
if (!scroller) return;
event.preventDefault();
snap = scroller.style.scrollSnapType;
scroller.style.scrollSnapType = "none";
scroller.addEventListener("scrollend", holdSnap, true);
Element.prototype.scrollTo.call(
scroller,
scroller.scrollLeft,
scroller.scrollTop,
);
startX = lastX = event.clientX;
startLeft = scroller.scrollLeft;
lastTime = performance.now();
velocity = 0;
};
const onMove = (event: MouseEvent) => {
if (!scroller) 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 = () => {
const target = scroller;
scroller = null;
if (!target) return;
const origin = Math.round(startLeft / ITEM_WIDTH);
const moved = (target.scrollLeft - startLeft) / ITEM_WIDTH;
const flick = Math.abs(velocity) > 0.3 ? -Math.sign(velocity) : 0;
const step = Math.abs(moved) > 0.3 ? Math.sign(moved) : flick;
const last = Math.round(
(target.scrollWidth - target.clientWidth) / ITEM_WIDTH,
);
const index = Math.min(last, Math.max(0, origin + step));
Element.prototype.scrollTo.call(target, {
left: index * ITEM_WIDTH,
behavior: "smooth",
});
target.style.scrollSnapType = snap;
target.removeEventListener("scrollend", holdSnap, true);
};
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);
scroller?.removeEventListener("scrollend", holdSnap, true);
};
}, []);
return hostRef;
}
export default function App() {
const custom = useCustomize(CUSTOMIZE);
const hostRef = useWebDragScroll();
useAutoplay(async ({ wait }) => {
for (const step of TOUR) {
await wait(step.hold);
scrollToIndex(hostRef.current as unknown as HTMLElement | null, step.index);
}
});
return (
<Showcase>
<View ref={hostRef} style={[styles.container, { backgroundColor: custom.background }]}>
<StatusBar style="light" />
<RotateCarousel
data={custom.photos.map((uri) => ({ image: { uri } }))}
renderItem={({ item }) => (
<Image
source={item.image}
style={[
styles.posterImage,
{
width: custom.posterWidth,
height: custom.posterHeight,
borderRadius: custom.radius,
},
]}
/>
)}
rotatePercentage={custom.rotatePercentage}
/>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
justifyContent: "center",
alignItems: "center",
},
posterImage: {
width: 250,
height: 390,
borderRadius: 16,
},
});dataТип readonly ItemT[]
renderItemТип (info: { item: ItemT; index: number }) => React.ReactNode
keyExtractor?Тип (item: ItemT, index: number) => string
spacing?Тип number
itemWidth?Тип number
horizontalSpacing?Тип number
rotatePercentage?Тип number
itemТип ItemT
indexТип number
scrollXТип SharedValue<number>
renderItemТип (info: { item: ItemT; index: number }) => ReactNode
spacing?Тип number
itemWidth?Тип number
rotatePercentage?Тип number