React Native使用PanResponder实现3个点的Slider

1,702 阅读3分钟

1. 前言

实现这个功能主要会使用RN原生API里的PanResponder。通过使用PanResponder,我们才能实现对整个拖动过程中的:拖动开始、拖动过程中、拖动结束,这3个关键点的重写。

2. 效果展示

image.png

3. 使用

import ThreeSlider from './threeSlider';

  /**
   * 开始移动时
   */
  onGrant() {
    this.setState({ isShowSliderText: true });
  }

  /**
   *   拖动结束的回调
   * @param {*} ishow
   */
  onRelease() {
    //隐藏
    this.setState({ isShowSliderText: false });
  }

  /**
   * A  移动时的回调
   */
  onStartMove(start) {
    console.log(start);
    this.setState({ start, sliderText: start });
  }

  /**
   * A点正在移动时Slider不在区域内
   */
  onStartSliderMove(start, slideValue) {
    this.setState({ start, slideValue, sliderText: start });
    this.player.seek(slideValue);
  }

  /**
   * B 移动时的回调
   */
  onEndMove(end) {
    this.setState({ end, sliderText: end });
  }

  /**
   * B点正在移动时Slider不在区域内
   */
  onEndSliderMove(end, slideValue) {
    this.setState({ end, slideValue, sliderText: end });

    this.player.seek(slideValue);
  }

  /**
   * 播放节点 移动时的回调
   */
  onSlideMove(slideValue) {
    this.setState({ slideValue, sliderText: slideValue });
    this.player.seek(slideValue);
  }

  /**
   * threeSlider View
   * @returns
   */
  threeSliderView = () => {
    return (
      <View style={{ flex: 1, height: 70 }}>
        <ThreeSlider
          range={this.state.duration}
          startA={this.state.start}
          endB={this.state.end}
          slideValue={this.state.slideValue}
          onGrant={this.onGrant.bind(this)} //开始移动的回调
          onRelease={this.onRelease.bind(this)} // 移动结束时的回调
          onStartMove={this.onStartMove.bind(this)} //A点正在移动时的回调
          onStartSliderMove={this.onStartSliderMove.bind(this)} //A点正在移动时Slider不在区域内
          onEndMove={this.onEndMove.bind(this)} //B点正在移动时的回调
          onEndSliderMove={this.onEndSliderMove.bind(this)} //B点正在移动时Slider不在区域内的回调
          onSlideMove={this.onSlideMove.bind(this)} //播放正在移动时的回调
        />
      </View>
    );
  };

3. 完整代码

//threeSlider.js

/**
 * 三个点的滑动进度条
 */

// Match参考 https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil
import React, { Component } from 'react';
import { StyleSheet, View, PanResponder, Text, Dimensions } from 'react-native';

const roundSize = 30; // 圆的大小
const width = Dimensions.get('window').width - roundSize * 2; // 设备宽度

export default class Index extends Component {
  constructor(props) {
    super(props);
    let scale = width / this.props.range;
    let { range, startA, endB, slideValue } = this.props;
    let start = Math.round(
      startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA
    );
    let end = Math.round(endB === 0 ? width : scale * endB);
    let slide = Math.round(
      slideValue === 0 ? roundSize / 2 : slideValue === range ? width - roundSize : scale * slideValue
    );
    this.state = {
      range,
      startA,
      endB,
      slideValue,
      start, // 起始坐标
      end, // 结束坐标
      slide
    };
  }

  //父元素对组件的props或state进行了修改
  UNSAFE_componentWillReceiveProps(nextProps) {
    let scale = width / this.props.range;
    let { range, startA, endB, slideValue } = nextProps;
    let start = Math.round(
      startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA
    );
    let slide = Math.round(
      slideValue === 0
        ? roundSize / 2
        : slideValue === range
        ? width - roundSize
        : scale * slideValue
    );
    let end = Math.round(endB === 0 ? width : scale * endB);
    this.setState({
      range,
      startA,
      endB,
      slideValue,
      start, // 起始坐标
      end, // 结束坐标
      slide
    });
  }

  //组件将要被加载到视图之前调用
  UNSAFE_componentWillMount() {
    let scale = width / this.props.range;
    this.panResponderStart = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      // 开始
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 开始的拖动事件
        let start = gestureState.moveX; // 当前拖动所在的坐标
        let threshold = this.state.end - roundSize; // 阀值
        if (start >= threshold) {
          // 保证开始价格不会超过结束价格
          start = threshold;
        }

        let startA = Math.round(start / scale); // 计算 实际的值
        // 保证开始价格不会小于最小值
        if (start <= roundSize) {
          start = roundSize / 2;
          startA = 0;
        }

        //修正播放的进度条
        if (start > this.state.slide) {
          //A点正在移动时Slider不在区域内
          let slideValue = Math.ceil(start / scale);
          this.setState({ slide: start, slideValue, start, startA }, () => {
            this.props.onStartSliderMove(this.state.startA, slideValue);
          });
        } else {
          this.setState(
            {
              start,
              startA
            },
            () => {
              this.props.onStartMove(this.state.startA);
            }
          );
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });
    this.panResponderEnd = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 结束的拖动事件
        let end = gestureState.moveX;
        let threshold = this.state.start + roundSize; // 阀值
        if (end <= threshold) {
          // 保证开始价格不会超过结束价格
          end = threshold;
        }
        // end = parseInt(end / step) * step;

        let endB = (end / scale).toFixed(2);
        if (end >= width) {
          // 保证结束价格不会超过最大值
          end = width;
          endB = this.state.range;
        }

        //修正播放的进度条
        if (end < this.state.slide) {
          let slideValue = Math.floor(end / scale);
          this.setState({ slide: end, slideValue });
        }

        if (end < this.state.slide) {
          //B点正在移动时Slider不在区域内
          let slideValue = Math.ceil(end / scale);
          this.setState({ slide: end, slideValue, end, endB }, () => {
            this.props.onEndSliderMove(this.state.endB, slideValue);
          });
        } else {
          this.setState(
            {
              end,
              endB
            },
            () => {
              this.props.onEndMove(this.state.endB);
            }
          );
        }
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });

    this.panResponderPlay = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        this.forceUpdate();
        //显示
        this.props.onGrant();
      },
      onPanResponderMove: (evt, gestureState) => {
        // 结束的拖动事件
        let slide = gestureState.moveX;
        let slideValue = Math.round(slide / scale);
        //最小值
        if (slide <= this.state.start) {
          // 保证开始价格不会小于最小值
          slide = this.state.start;
          slideValue= this.state.startA
        }

        //最大值
        if (slide >= this.state.end) {
          // 保证开始价格不会大于最大值
          slide = this.state.end;
          slideValue = this.state.range
        }

        this.setState(
          {
            slide,
            slideValue
          },
          () => {
            this.props.onSlideMove(this.state.slideValue);
          }
        );
      },
      onPanResponderRelease: (evt, gestureState) => {
        // 隐藏
        this.props.onRelease();
        return true;
      },
      onPanResponderTerminate: (evt, gestureState) => true
    });
  }

  render() {
    let { start, end, slide } = this.state;
    return (
      <View style={styles.container}>
        <View style={{ flexDirection: 'row' }}>
          <View
            style={[
              styles.progressContainer,
              { backgroundColor: '#D6D7E6' },
              { width: start == roundSize / 2 ? 0 : start }
            ]}
          />
          <View style={[styles.progressContainer, { width: width - start - (width - end) }]} />
          <View
            style={[
              styles.progressContainer,
              { backgroundColor: '#D6D7E6' },
              { width: width - end }
            ]}
          />
        </View>

        <View style={[styles.startA, { left: start }]} {...this.panResponderStart.panHandlers}>
          <Text>A</Text>
        </View>
        <View style={[styles.endB, { left: end }]} {...this.panResponderEnd.panHandlers}>
          <Text>B</Text>
        </View>
        <View style={[styles.slide, { left: slide }]} {...this.panResponderPlay.panHandlers}></View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    height: 70,
    justifyContent: 'center',
    alignItems: 'center'
  },
  progressContainer: {
    backgroundColor: '#ffa710',
    height: 4
  },
  startA: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: 'green',
    top: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  endB: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: 'yellow',
    top: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  slide: {
    position: 'absolute',
    width: roundSize,
    height: roundSize,
    borderRadius: roundSize / 2,
    borderColor: '#D6D7E6',
    borderWidth: 1,
    shadowColor: 'rgba(0,0,0,0.6)',
    shadowRadius: 5,
    shadowOpacity: 0.9,
    backgroundColor: '#094D45',
    bottom: 1
  }
});

//默认props
Index.defaultProps = {
  range: 1000, // 默认
  startA: 0, // 起始
  endB: width
};

4. 使用

react native 滑动价格区间

拖动动画支持