import React, { forwardRef, useMemo, useRef } from 'react'; import { View, Pressable, PanResponder } from 'react-native'; import type { GestureResponderEvent, LayoutChangeEvent } from 'react-native'; import Icon from '@pingtou/rn-vant-icons'; import { useThemeFactory } from '../Theme'; import type { RateProps } from './type'; import { createStyle } from './style'; import { useControllableValue } from '../hooks'; type RateStatus = 'full' | 'half' | 'void'; type RateListItem = { value: number; status: RateStatus; }; function getRateStatus( value: number, index: number, allowHalf?: boolean, readonly?: boolean ): RateListItem { if (value >= index) { return { status: 'full', value: 1 }; } if (value + 0.5 >= index && allowHalf && !readonly) { return { status: 'half', value: 0.5 }; } if (value + 1 >= index && allowHalf && readonly) { const cardinal = 10 ** 10; return { status: 'half', value: Math.round((value - index + 1) * cardinal) / cardinal, }; } return { status: 'void', value: 0 }; } const Rate = forwardRef((props, ref) => { const { count = 5, icon = 'star', voidIcon = 'star-o', touchable = true, allowHalf, readonly, disabled, style, } = props; const [value, setValue] = useControllableValue(props); const { styles, theme } = useThemeFactory(createStyle); /** * 将每个 star 相对于父组件的 X 轴坐标和对应的评分保存到这里 * 用于手势划过时,判断当前划过的到底是哪个 star */ const ranges = useRef>({}); // 容器的 X 轴坐标 const containerX = useRef(0); const { size = theme.radio_icon_size, gutter = theme.rate_icon_gutter, color = theme.rate_icon_full_color, voidColor = theme.rate_icon_void_color, disabledColor = theme.rate_icon_disabled_color, } = props; // 是否允许触摸拖动 const untouchable = readonly || disabled || touchable; const list = useMemo(() => { return Array(count) .fill('') .map((_, i) => getRateStatus(value, i + 1, allowHalf, readonly)); }, [value, count]); const renderStar = (item: RateListItem, index: number) => { const isFull = item.status === 'full'; const isVoid = item.status === 'void'; const renderHalf = allowHalf && item.value > 0 && item.value < 1; const onClickItem = (event: GestureResponderEvent) => { if (allowHalf) { const { locationX } = event.nativeEvent; const halfScore = locationX < size / 2 ? 0.5 : 1; setValue(index + halfScore); } else { setValue(index + 1); } }; const onLayout = (event: LayoutChangeEvent) => { ranges.current[index] = { left: event.nativeEvent.layout.x, score: index + 1, }; }; return ( 0 ? { marginLeft: gutter } : undefined]} onPress={onClickItem} disabled={disabled || readonly} onLayout={onLayout} > {renderHalf && ( )} ); }; // 根据触摸的距离算出评分 const getScoreByPosition = (x: number): number => { for (let i = count - 1; i > 0; i--) { if (x > ranges.current[i].left) { return ranges.current[i].score; } } return allowHalf ? 0.5 : 1; }; const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderMove: (_evt, gestureState) => { // 手势触摸点距离容器左侧的距离 const locationX = gestureState.moveX - containerX.current; const score = getScoreByPosition(locationX); setValue(score); }, }) ).current; const onContainerLayout = (event: LayoutChangeEvent) => { containerX.current = event.nativeEvent.layout.x; }; return ( {list.map(renderStar)} ); }); Rate.displayName = 'Rate'; export default Rate;