import React, { useRef, useEffect, useState } from 'react'; import { PanResponder, StyleSheet, View, Animated, Easing, ViewProps, } from 'react-native'; import Star from './Star'; import { getStars } from './utils'; import type { Theme } from './../../theme/theme'; import { createBox } from '@shopify/restyle'; type AnimationConfig = { easing?: (value: number) => number; duration?: number; delay?: number; scale?: number; }; type StarRatingProps = { rating: number; onChange?: (rating: number) => void | undefined; minRating?: number; maxStars?: number; starColor?: string; starSize?: number; enableSwiping?: boolean; animationConfig?: AnimationConfig; testID?: string; }; const defaultAnimationConfig: Required = { easing: Easing.elastic(2), duration: 300, scale: 1.2, delay: 300, }; const ViewComponent = createBox< Theme, ViewProps & { children?: React.ReactNode } >(View); export const StarRating: React.FC = ({ rating, maxStars = 5, starColor = '#FFB800', starSize = 24, onChange, enableSwiping = true, animationConfig = defaultAnimationConfig, testID, }) => { const width = useRef(); const ref = useRef(null); const [isInteracting, setInteracting] = useState(false); const handleInteraction = (x: number) => { if (width.current) { const newRating = Math.max( 0, Math.min( Math.round((x / width.current) * maxStars * 2 + 0.2) / 2, maxStars ) ); if (onChange !== undefined) { onChange(Math.ceil(newRating)); } } }; const [panResponder] = useState(() => PanResponder.create({ onStartShouldSetPanResponder: () => true, onStartShouldSetPanResponderCapture: () => true, onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderMove: (e) => { if (enableSwiping) { handleInteraction(e.nativeEvent.locationX); } }, onPanResponderStart: (e) => { handleInteraction(e.nativeEvent.locationX); if (onChange !== undefined) { setInteracting(true); } }, onPanResponderEnd: () => { setTimeout(() => { setInteracting(false); }, animationConfig.delay || defaultAnimationConfig.delay); }, }) ); return ( { if (ref.current) { ref.current.measure((_x, _y, w, _h) => (width.current = w)); } }} testID={testID} > {getStars(rating, maxStars).map((starType, i) => { return ( = 0} animationConfig={animationConfig} > ); })} ); }; type AnimatedIconProps = { active: boolean; children: React.ReactElement; animationConfig: AnimationConfig; }; const AnimatedIcon: React.FC = ({ active, animationConfig, children, }) => { const { scale = defaultAnimationConfig.scale, easing = defaultAnimationConfig.easing, duration = defaultAnimationConfig.duration, } = animationConfig; const animatedSize = useRef(new Animated.Value(active ? scale : 1)); useEffect(() => { const animation = Animated.timing(animatedSize.current, { toValue: active ? scale : 1, useNativeDriver: true, easing, duration, }); animation.start(); return animation.stop; }, [active, scale, easing, duration]); return ( {children} ); }; const styles = StyleSheet.create({ star: { marginHorizontal: 1, }, });