ReactNative实现弧形拖动条

我们直接看效果

先看下面的使用代码

     <CircularSlider5
          step={2}
          min={0}
          max={100}
          radius={100}
          value={30}
          onComplete={(changeValue: number) => this.handleEmailSbp(changeValue)}
          onChange={(changeValue: number) => this.handleEmailDpd(changeValue)}
          contentContainerStyle={styles.contentContainerStyle}
          strokeWidth={10}
          buttonBorderColor="#3FE3EB"
          buttonFillColor="#fff"
          buttonStrokeWidth={10}
          openingRadian={Math.PI / 4}
          buttonRadius={10}
          triangleLinerGradient={[
            {stop: '0%', color: '#FF7B4C'},
            {stop: '50%', color: '#FFFFFF'},
            {stop: '100%', color: '#317AF7'},
          ]}
          linearGradient={[
            {stop: '0%', color: '#3FE3EB'},
            {stop: '100%', color: '#7E84ED'},
          ]}></CircularSlider5>

{

radius: 100, // 半径

strokeWidth: 20, // 线宽

openingRadian: Math.PI / 4, // 开口弧度,为了便于计算值为实际开口弧度的一半

backgroundTrackColor: '#e8e8e8', // 底部轨道颜色

linearGradient: [

{stop: '0%', color: '#1890ff'},

{stop: '100%', color: '#f5222d'},

], // 渐变色

min: 0, // 最小值

max: 100, // 最大值

buttonRadius: 12, // 按钮半径

buttonBorderColor: '#fff', // 按钮边框颜色

buttonStrokeWidth: 1, // 按钮线宽

};

本组件使用到了

1.react-native-svg

2.PanResponder

具体代码如下

import React, {PureComponent} from 'react';
import Svg, {
  Path,
  G,
  Defs,
  LinearGradient,
  Stop,
  Circle,
} from 'react-native-svg';
import {StyleSheet, View, PanResponder} from 'react-native';

export default class CircularSlider extends PureComponent {
  static defaultProps = {
    radius: 100, // 半径
    strokeWidth: 20, // 线宽
    openingRadian: Math.PI / 4, // 开口弧度,为了便于计算值为实际开口弧度的一半
    backgroundTrackColor: '#e8e8e8', // 底部轨道颜色
    linearGradient: [
      {stop: '0%', color: '#1890ff'},
      {stop: '100%', color: '#f5222d'},
    ], // 渐变色
    min: 0, // 最小值
    max: 100, // 最大值
    buttonRadius: 12, // 按钮半径
    buttonBorderColor: '#fff', // 按钮边框颜色
    buttonStrokeWidth: 1, // 按钮线宽
  };

  constructor(props) {
    super(props);
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => false,
      onPanResponderGrant: this._handlePanResponderGrant,
      onPanResponderMove: this._handlePanResponderMove,
      onPanResponderRelease: this._handlePanResponderEnd,
      onPanResponderTerminationRequest: () => false,
      onPanResponderTerminate: this._handlePanResponderEnd,
    });

    this.state = {
      value: props.value || props.min,
    };

    this._containerRef = React.createRef();
  }

  _handlePanResponderGrant = () => {
    /*
     * 记录开始滑动开始时的滑块值、弧度和坐标,用户后续值的计算
     */
    const {value} = this.state;
    this._moveStartValue = value;
    // 获取开始移动的弧度
    this._moveStartRadian = this.getRadianByValue(value);
    // 根据弧度获取开始的极坐标
    this._startCartesian = this.polarToCartesian(this._moveStartRadian);
    // console.log(`开始滑动弧度${this._startCartesian}`);
    // console.log(`开始滑动${this._startCartesian.x}:${this._startCartesian.y}`);
  };

  _handlePanResponderMove = (e, gestureState) => {
    const {min, max, step, openingRadian} = this.props;
    let {x, y} = this._startCartesian;
    x += gestureState.dx;
    y += gestureState.dy;
    // console.log(`滑动过程中${x}:${y}`);
    const radian = this.cartesianToPolar(x, y); // 当前弧度
    console.log(`滑动过程中的弧度${radian}`);
    const ratio =
      (this._moveStartRadian - radian) / ((Math.PI - openingRadian) * 2); // 弧度变化所占比例
    const diff = max - min; // 最大值和最小值的差

    let value;
    if (step) {
      value = this._moveStartValue + Math.round((ratio * diff) / step) * step;
    } else {
      value = this._moveStartValue + ratio * diff;
    }
    // 处理极值
    value = Math.max(min, Math.min(max, value));
    this.setState({
      value,
    });
    // this.setState(({value: curValue}) => {
    //   value = Math.abs(value - curValue) > diff / 4 ? curValue : value; // 避免直接从最小值变为最大值
    //   return {value: Math.round(value)};
    // });
    this._fireChangeEvent('onChange');
  };

  _handlePanResponderEnd = (e, gestureState) => {
    if (this.props.disabled) {
      return;
    }

    this._fireChangeEvent('onComplete');
  };

  _fireChangeEvent = event => {
    if (this.props[event]) {
      this.props[event](this.state.value);
    }
  };

  /**
   * 极坐标转笛卡尔坐标
   * @param {number} radian - 弧度表示的极角
   */
  polarToCartesian(radian) {
    const {radius} = this.props;
    const distance = radius + this._getExtraSize() / 2; // 圆心距离坐标轴的距离
    const x = distance + radius * Math.sin(radian);
    const y = distance + radius * Math.cos(radian);
    return {x, y};
  }

  /**
   * 笛卡尔坐标转极坐标
   * @param {*} x
   * @param {*} y
   */
  cartesianToPolar(x, y) {
    const {radius} = this.props;
    const distance = radius + this._getExtraSize() / 2; // 圆心距离坐标轴的距离
    if (x === distance) {
      return y > distance ? 0 : Math.PI / 2;
    }
    const a = Math.atan((y - distance) / (x - distance)); // 计算点与圆心连线和 x 轴的夹角
    return (x < distance ? (Math.PI * 3) / 2 : Math.PI / 2) - a;
  }

  /**
   * 获取当前弧度
   */
  getCurrentRadian() {
    return this.getRadianByValue(this.state.value);
  }

  /**
   * 根据滑块的值获取弧度
   * @param {*} value
   */
  getRadianByValue(value) {
    const {openingRadian, min, max} = this.props;
    return (
      ((Math.PI - openingRadian) * 2 * (max - value)) / (max - min) +
      openingRadian
    );
  }

  /**
   * 获取除半径外额外的大小,返回线宽和按钮直径中较大的
   */
  _getExtraSize() {
    const {strokeWidth, buttonRadius, buttonStrokeWidth} = this.props;
    return Math.max(strokeWidth, (buttonRadius + buttonStrokeWidth) * 2);
  }

  _onLayout = () => {
    const ref = this._containerRef.current;
    if (ref) {
      ref.measure((x, y, width, height, pageX, pageY) => {
        this.vertexX = pageX;
        this.vertexY = pageY;
      });
    }
  };

  render() {
    const {
      radius,
      strokeWidth,
      backgroundTrackColor,
      openingRadian,
      linearGradient,
      buttonRadius,
      buttonBorderColor,
      buttonFillColor,
      buttonStrokeWidth,
      style,
      contentContainerStyle,
      children,
    } = this.props;
    const svgSize = radius * 2 + this._getExtraSize();
    const startRadian = 2 * Math.PI - openingRadian; // 起点弧度
    const startPoint = this.polarToCartesian(startRadian);
    const endPoint = this.polarToCartesian(openingRadian);
    const currentRadian = this.getCurrentRadian(); // 当前弧度
    const curPoint = this.polarToCartesian(currentRadian);

    const contentStyle = [styles.content, contentContainerStyle];

    return (
      <View
        onLayout={this._onLayout}
        ref={this._containerRef}
        style={[styles.container, style]}>
        <Svg width={svgSize} height={svgSize}>
          <Defs>
            <LinearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="gradient">
              {linearGradient.map((item, index) => (
                <Stop key={index} offset={item.stop} stopColor={item.color} />
              ))}
            </LinearGradient>
          </Defs>
          <G rotation={0} origin={`${svgSize / 2}, ${svgSize / 2}`}>
            <Path
              strokeWidth={strokeWidth}
              stroke={backgroundTrackColor}
              fill="none"
              strokeLinecap="round"
              d={`M${startPoint.x},${startPoint.y} A ${radius},${radius},0,${
                startRadian - openingRadian >= Math.PI ? '1' : '0'
              },1,${endPoint.x},${endPoint.y}`}
            />
            <Path
              strokeWidth={strokeWidth}
              stroke="url(#gradient)"
              fill="none"
              strokeLinecap="round"
              d={`M${startPoint.x},${startPoint.y} A ${radius},${radius},0,${
                startRadian - currentRadian >= Math.PI ? '1' : '0'
              },1,${curPoint.x},${curPoint.y}`}
            />
            <Circle
              cx={curPoint.x}
              cy={curPoint.y}
              r={buttonRadius}
              fill={buttonFillColor || buttonBorderColor}
              stroke={buttonBorderColor}
              strokeWidth={buttonStrokeWidth}
              {...this._panResponder.panHandlers}
            />
          </G>
        </Svg>
        <View style={contentStyle} pointerEvents="box-none">
          {children}
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  content: {
    position: 'absolute',
    left: 0,
    top: 0,
    bottom: 0,
    right: 0,
  },
});
相关推荐
wakangda9 小时前
React Native 集成 iOS 原生功能
react native·ios·cocoa
SuperHeroWu79 小时前
【HarmonyOS】HarmonyOS和React Native混合开发 (一)之环境安装
react native·harmonyos·鸿蒙·开发环境·环境安装·rn·混合开发
wakangda10 小时前
React Native 集成原生Android功能
javascript·react native·react.js
爱lv行5 天前
生态:React Native
javascript·react native·react.js
少恭写代码6 天前
duxapp 2024-12-18更新 新增 Svg 组件 Tab 支持幻灯片切换功能
react native·小程序·taro
凌鲨7 天前
React Native学习路线图
学习·react native·react.js
一个处女座的程序猿O(∩_∩)O7 天前
四大跨平台开发框架深度解析——uniapp、uniapp-X、React Native与Flutter
flutter·react native·uni-app
苍岚丨晨苏7 天前
使用Taro开发iOS App触发额外权限请求的问题
react native·taro
Domain-zhuo9 天前
如何理解React State不可变性的原则
前端·javascript·react native·react.js·前端框架·ecmascript
wuwuFQ11 天前
React Native状态管理器Redux、MobX、Context API、useState
javascript·react native·react.js