0

我构建了一个模仿 iOS 原生 Slider 的 Slider 组件(Single 和 Range)。我是使用动画和 PanResponder 的新手,所以不完全确定它是如何工作的 + 将它与 useCallback 和 useMemo 一起使用。

下面是该组件的代码,但我还将 GitHub Repo 与导入的组件链接以进行测试。有人可以查看我的代码/给我提示以增强 Slider 组件吗?

GitHub 回购:https ://github.com/jefelewis/react-native-slider

滑块.tsx

// Imports: Dependencies
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Animated, Dimensions, PanResponder, PanResponderGestureState, StyleSheet, View } from 'react-native';

// Imports: React Hooks
import { useLowHigh, useWidthLayout, useSelectedRail } from './hooks/hooks';

// Imports: Helper Functions
import { clamp, getValueForPosition, isLowCloser } from './helpers/helpers';

// Imports: Styles
import { defaultStyles } from '../../styles/styles';

// TypeScript Type: Props
interface IProps {
  type: 'Range' | 'Single';
  min: number;
  max: number;
  step: number;
  onChange: (low: number, high: number) => void;
  minRange?: number;
  disabled?: boolean;
  darkMode?: boolean;
}

// TypeScript Type: Gesture State Ref
interface IGestureStateRef {
  isLow: boolean;
  lastValue: number;
  lastPosition: number;
}

// React Native: Screen Dimensions
const { width } = Dimensions.get('window');

// Component: Slider
export const Slider = ({ type, min, max, step, onChange, minRange = 0, disabled = false, darkMode = false }: IProps): JSX.Element => {
  // React Hooks: State
  const [thumbWidth, setThumbWidth] = useState<number>(0);

  // TODO TODO TODO (SET STATE + FIX OVERLAP WITH NAMES)
  const [lowProp, setLowProp] = useState<number>(min);
  const [highProp, setHighProp] = useState<number>(max);

  // React Hooks: Refs
  const lowThumbXRef = useRef<Animated.Value>(new Animated.Value(0));
  const highThumbXRef = useRef<Animated.Value>(new Animated.Value(0));
  const pointerX = useRef<Animated.Value>(new Animated.Value(0)).current;
  const gestureStateRef = useRef<IGestureStateRef>({ isLow: true, lastValue: 0, lastPosition: 0 });
  const containerWidthRef = useRef<number>(0);

  // React Hooks: Use Low High
  const { inPropsRef, inPropsRefPrev, setLow, setHigh } = useLowHigh(lowProp, type === 'Single' ? max : highProp, min, max, step);

  // React Hooks: Use Selected Rail
  const [selectedRailStyle, updateSelectedRail] = useSelectedRail(inPropsRef, containerWidthRef, thumbWidth, type);

  // Update Thumbs
  const updateThumbs = useCallback(() => {
    // Ref: Container With
    const { current: containerWidth } = containerWidthRef;

    if (!thumbWidth || !containerWidth) {
      return;
    }

    // Ref: Props
    const { low, high } = inPropsRef.current;

    // Slider Type: Range
    if (type === 'Range') {
      // High Position
      const highPosition: number = ((high - min) / (max - min)) * (containerWidth - thumbWidth);

      // Ref: Set Value (High Thumb X)
      highThumbXRef.current.setValue(highPosition);
    }

    // Low Position
    const lowPosition: number = ((low - min) / (max - min)) * (containerWidth - thumbWidth);

    // Ref: Set Value (Low Thumb X)
    lowThumbXRef.current.setValue(lowPosition);

    // Update Selected Rail
    updateSelectedRail();

    // Props: On Change
    onChange?.(low, high);
  }, [type, inPropsRef, max, min, onChange, thumbWidth, updateSelectedRail]);

  // React Hooks: Lifecycle Methods
  useEffect(() => {
    // TODO (WHAT?)
    if ((lowProp !== undefined && lowProp !== inPropsRefPrev.lowPrev) || (highProp !== undefined && highProp !== inPropsRefPrev.highPrev)) {
      // Update Thumbs
      updateThumbs();
    }
  }, [highProp, inPropsRefPrev.lowPrev, inPropsRefPrev.highPrev, lowProp, inPropsRefPrev, updateThumbs]);

  useEffect(() => {
    // Update Thumbs
    updateThumbs();
  }, [updateThumbs]);

  // Handle Container Layout
  const handleContainerLayout = useWidthLayout(containerWidthRef, updateThumbs);

  // Handle Thumb Layout
  const handleThumbLayout = useCallback(
    ({ nativeEvent }) => {
      const {
        layout: { width: newWidth },
      } = nativeEvent;

      if (thumbWidth !== newWidth) {
        setThumbWidth(newWidth);
      }
    },
    [thumbWidth],
  );
  // React Native: Pan Handlers
  const { panHandlers } = useMemo(
    () =>
      // React Native: Pan Responder
      PanResponder.create({
        // On Start Should Set Pan Responder
        onStartShouldSetPanResponder: (): boolean => true,
        // On Start Should Set Pan Responder Capture
        onStartShouldSetPanResponderCapture: (): boolean => true,
        // On Move Should Set Pan Responder
        onMoveShouldSetPanResponder: (): boolean => true,
        // On Move Should Set Pan Responder Capture
        onMoveShouldSetPanResponderCapture: (): boolean => true,
        // On Pan Responder Termination Request
        onPanResponderTerminationRequest: (): boolean => true,
        // On Pan Responder Terminate
        onPanResponderTerminate: (): boolean => true,
        // On Should Block Native Responder
        onShouldBlockNativeResponder: (): boolean => true,
        // On Pan Responder Grant
        onPanResponderGrant: ({ nativeEvent }, gestureState: PanResponderGestureState): void => {
          // TODO (WHAT?) (REFACTOR WITH !disabled)
          if (disabled) {
            return;
          }

          // Gesture State: Number Of Active Touches (Currently On Screen)
          const { numberActiveTouches } = gestureState;

          if (numberActiveTouches > 1) {
            return;
          }

          // TODO (WHAT?)
          const { locationX: downX, pageX } = nativeEvent;

          // Container X
          const containerX: number = pageX - downX;

          // TODO (WHAT?)
          const { low, high, min, max } = inPropsRef.current;

          // Container Width
          const containerWidth: number = containerWidthRef.current;

          // Low Position
          const lowPosition: number = thumbWidth / 2 + ((low - min) / (max - min)) * (containerWidth - thumbWidth);

          // High Position
          const highPosition: number = thumbWidth / 2 + ((high - min) / (max - min)) * (containerWidth - thumbWidth);

          // Is Low
          const isLow: boolean = type === 'Single' || isLowCloser(downX, lowPosition, highPosition);

          // TODO (WHAT?)
          gestureStateRef.current.isLow = isLow;

          // Handle Position Change
          const handlePositionChange = (positionInView: number): void => {
            // TODO (WHAT?)
            const { low, high, min, max, step } = inPropsRef.current;

            // Min Value
            const minValue: number = isLow ? min : low + minRange;

            // Max Value
            const maxValue: number = isLow ? high - minRange : max;

            // Value
            const value: number = clamp(getValueForPosition(positionInView, containerWidth, thumbWidth, min, max, step), minValue, maxValue);

            // TODO (WHAT?)
            if (gestureStateRef.current.lastValue === value) {
              return;
            }

            // Available Space
            const availableSpace: number = containerWidth - thumbWidth;

            // Absolute Position
            const absolutePosition: number = ((value - min) / (max - min)) * availableSpace;

            // Ref: Last Value (Gesture State)
            gestureStateRef.current.lastValue = value;

            // Ref: Last Position (Gesture State)
            gestureStateRef.current.lastPosition = absolutePosition + thumbWidth / 2;

            // Ref: Set Value (Absolute Position)
            (isLow ? lowThumbXRef.current : highThumbXRef.current).setValue(absolutePosition);

            // Props: On Change
            onChange?.(isLow ? value : low, isLow ? high : value);

            // TODO (WHAT?)
            (isLow ? setLow : setHigh)(value);

            // Update Selected Rail
            updateSelectedRail();
          };

          // Handle Position Change
          handlePositionChange(downX);

          // Ref: Remove All Listeners (Pointer X)
          pointerX.removeAllListeners();

          // Ref: Add Listener (Pointer X)
          pointerX.addListener(({ value: pointerPosition }): void => {
            // Position In View
            const positionInView: number = pointerPosition - containerX;

            // Handle Position Change
            handlePositionChange(positionInView);
          });
        },
        // On Pan Responder Move
        onPanResponderMove: disabled ? undefined : Animated.event([null, { moveX: pointerX }], { useNativeDriver: false }),
        // On Pan Responder Release
        onPanResponderRelease: (): void => {
          return;
        },
      }),
    [disabled, pointerX, inPropsRef, thumbWidth, type, minRange, onChange, setLow, setHigh, updateSelectedRail],
  );

  return (
    <View style={{ width: width - 32 }}>
      <View onLayout={handleContainerLayout} style={styles.controlsContainer}>
        <View style={[styles.railsContainer, { marginHorizontal: thumbWidth / 2 }]}>
          <View style={[styles.railContainer, darkMode ? styles.railContainerDark : styles.railContainerLight]} />

          <Animated.View style={selectedRailStyle}>
            <View style={[styles.railSelectedContainer, darkMode ? styles.railSelectedContainerDark : styles.railSelectedContainerLight]} />
          </Animated.View>
        </View>

        <Animated.View style={{ transform: [{ translateX: lowThumbXRef.current }] }} onLayout={handleThumbLayout}>
          <View style={[styles.thumbContainer, darkMode ? styles.thumbContainerDark : styles.thumbContainerLight]} />
        </Animated.View>

        {type === 'Range' && (
          <Animated.View style={[styles.highThumbContainer, { transform: [{ translateX: highThumbXRef.current }] }]}>
            <View style={[styles.thumbContainer, darkMode ? styles.thumbContainerDark : styles.thumbContainerLight]} />
          </Animated.View>
        )}

        <View {...panHandlers} style={styles.touchableArea} collapsible={false} />
      </View>
    </View>
  );
};

// Styles
const styles = StyleSheet.create({
  controlsContainer: {
    flexDirection: 'row',
    justifyContent: 'flex-start',
    alignItems: 'center',
  },
  highThumbContainer: {
    position: 'absolute',
  },
  railsContainer: {
    ...StyleSheet.absoluteFillObject,
    flexDirection: 'row',
    alignItems: 'center',
  },
  labelFixedContainer: {
    alignItems: 'flex-start',
  },
  labelFloatingContainer: {
    position: 'absolute',
    alignItems: 'flex-start',
    left: 0,
    right: 0,
  },
  touchableArea: {
    ...StyleSheet.absoluteFillObject,
  },
  railContainer: {
    flex: 1,
    height: 2,
    borderRadius: 2,
  },
  railContainerLight: {
    backgroundColor: defaultStyles.colorLightBorder,
  },
  railContainerDark: {
    backgroundColor: defaultStyles.colorDarkBorder,
  },
  railSelectedContainer: {
    height: 2,
    borderRadius: 2,
  },
  railSelectedContainerLight: {
    backgroundColor: defaultStyles.colorLightBlue,
  },
  railSelectedContainerDark: {
    backgroundColor: defaultStyles.colorDarkBlue,
  },
  thumbContainer: {
    height: 30,
    width: 30,
    backgroundColor: '#FFFFFF',
    borderRadius: 30,
    borderWidth: 0.5,
    shadowOffset: {
      width: 0,
      height: 3,
    },
    shadowRadius: 1,
    shadowOpacity: 0.1,
  },
  thumbContainerLight: {
    borderColor: defaultStyles.colorLightBorder,
  },
  thumbContainerDark: {
    borderColor: defaultStyles.colorDarkBorder,
  },
});
4

0 回答 0