expo-popcore-app/components/ui/carousel.tsx

152 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef } from "react";
import { View as RNView, Dimensions, type ViewProps, type StyleProp, type ViewStyle } from "react-native";
import RNCarousel, { ICarouselInstance } from "react-native-reanimated-carousel";
import { cn } from "../../lib/utils";
// 扩展 View 以支持 classNameNativeWind
const View = RNView as React.ComponentType<ViewProps & { className?: string }>;
const { width: screenWidth } = Dimensions.get("window");
export type CarouselApi = ICarouselInstance;
type CarouselProps = {
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
className?: string;
children: React.ReactNode;
width?: number;
height?: number;
autoPlay?: boolean;
autoPlayInterval?: number;
loop?: boolean;
};
type CarouselContextProps = {
api: CarouselApi | null;
orientation: "horizontal" | "vertical";
width: number;
height: number;
autoPlay: boolean;
autoPlayInterval: number;
loop: boolean;
};
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
setApi,
className,
children,
width = screenWidth,
height = 410,
autoPlay = false,
autoPlayInterval = 3000,
loop = true,
onIndexChange,
}: CarouselProps & { onIndexChange?: (index: number) => void }) {
const [api, setInternalApi] = useState<CarouselApi | null>(null);
const carouselRef = useRef<ICarouselInstance>(null);
React.useEffect(() => {
if (carouselRef.current) {
const instance = carouselRef.current;
setInternalApi(instance);
setApi?.(instance);
}
}, [setApi]);
// 提取子元素到数组,过滤掉非 ReactElement 的内容
// 如果子元素是 CarouselContent需要提取其内部的子元素
const childrenArray = React.Children.toArray(children);
const items = childrenArray.reduce<React.ReactElement[]>((acc, child) => {
if (React.isValidElement(child)) {
const props = child.props as { children?: React.ReactNode };
if (props.children) {
const innerChildren = React.Children.toArray(props.children).filter(
(innerChild): innerChild is React.ReactElement => React.isValidElement(innerChild)
);
return [...acc, ...innerChildren];
}
return [...acc, child];
}
return acc;
}, []);
return (
<CarouselContext.Provider
value={{
api,
orientation,
width,
height,
autoPlay,
autoPlayInterval,
loop,
}}
>
<View className={cn("relative", className)}>
<RNCarousel
ref={carouselRef}
loop={loop}
width={width}
height={height}
autoPlay={autoPlay}
autoPlayInterval={autoPlayInterval}
data={items}
scrollAnimationDuration={500}
vertical={orientation === "vertical"}
renderItem={({ item }) => item as React.ReactElement}
onSnapToItem={onIndexChange}
/>
</View>
</CarouselContext.Provider>
);
}
function CarouselContent({
children,
...props
}: {
className?: string;
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}) {
// CarouselContent 在新架构中主要用于包裹 CarouselItem
// 实际渲染由 Carousel 的 renderItem 处理
return <>{children}</>;
}
function CarouselItem({
className,
children,
...props
}: {
className?: string;
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}) {
const { width, height } = useCarousel();
return (
<View
className={cn("flex justify-center items-center", className)}
style={{ width, height }}
{...props}
>
{children}
</View>
);
}
export { Carousel, CarouselContent, CarouselItem, useCarousel };