javascript - 如何补偿圆形 slider 组件中的旋转容器 View ?

标签 javascript typescript react-native svg react-native-svg

我已经成功地从各种部分记录的代码片段(主要是 here )拼凑出一个圆形 slider 组件。我已经整理了一些东西,现在有一些可以用作可滑动控制转盘的东西,我可以在其中为可滑动区域设置任意最大 Angular :

Dial with no rotation and maxAngle set to 270

但是,我想要更多的灵活性 - 我也希望能够选择任意的起始 Angular 。例如,这将使我能够创建有点像汽车仪表板控件的表盘。下面显示的是我尝试让它工作的尝试。正如您所看到的,这扰乱了触摸处理:

enter image description here

显然我需要补偿触摸处理中的旋转。

我通过将 SVG 组件(包含此处显示的所有图形和文本)封装在 View 中来实现这一切。对于我遇到问题的示例,我旋转容器 View (也要小心地将旋转值传递给文本,以便将该文本旋转回垂直位置)。

typescript 组件:

import React, { Component } from 'react';
import ReactNative, { PanResponder, View, Dimensions } from 'react-native';
import Svg, {
  Path,
  Circle,
  G,
  Text,
  Defs,
  LinearGradient,
  Stop,
  Linecap
} from 'react-native-svg';

interface Props {
  value: number;
  dialRadius: number;
  btnRadius: number;
  startCoord: number;
  startGradient: string;
  endGradient: string;
  dialWidth: number;
  cap: Linecap;
  dialBgWidth: number;
  backgroundColor: string;
  textSize: number;
  textColor: string;
  showValue: boolean;
  btnFill: string;
  maxAngle: number;
  rotationOffset: number;
  onValueChange (angle: number): void;
}

interface State {
  angle: number;
  xCenter: number;
  yCenter: number;
}

export default class CircularSlider extends Component<Props, State> {
  static defaultProps = {
    btnRadius: 10,
    dialRadius: 80,
    dialWidth: 20,
    textColor: 'white',
    textSize: 30,
    value: 0,
    showValue: true,
    startGradient: '#12D8FA',
    endGradient: '#12D8FA',
    backgroundColor: 'white',
    startCoord: 0,
    cap: 'butt',
    btnFill: 'transparent',
    dialBgWidth: 20,
    maxAngle: 360,
    onValueChange: null,
    rotationOffset: 0
  };

  _panResponder: any;
  circleSlider: any;
  container: any;

  constructor (props: any) {
    super(props);

    this.state = {
      angle: this.props.value,
      xCenter: 0,
      yCenter: 0
    };

    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (e, gs) => true,
      onStartShouldSetPanResponderCapture: (e, gs) => true,
      onMoveShouldSetPanResponder: (e, gs) => true,
      onMoveShouldSetPanResponderCapture: (e, gs) => true,
      onPanResponderMove: (e, gs) => {
        let xOrigin =
          this.state.xCenter - (this.props.dialRadius + this.props.btnRadius);
        let yOrigin =
          this.state.yCenter - (this.props.dialRadius + this.props.btnRadius);
        let a = this.cartesianToPolar(gs.moveX - xOrigin, gs.moveY - yOrigin);
        this.setState({ angle: Math.min(a, this.props.maxAngle) });
      }
    });
  }

  polarToCartesian (angle: number) {
    let r = this.props.dialRadius;
    let hC = this.props.dialRadius + this.props.btnRadius;
    let a = ((angle - (90)) * Math.PI) / (180.0) ;

    let x = hC + r * Math.cos(a);
    let y = hC + r * Math.sin(a);
    return { x, y };
  }

  cartesianToPolar (x: number, y: number) {
    let hC = this.props.dialRadius + this.props.btnRadius;
    if (x === 0) {
      return y > hC ? 0 : 180;
    } else if (y === 0) {
      return x > hC ? 90 : 270;
    } else {
      return (
        Math.round((Math.atan((y - hC) / (x - hC)) * 180) / Math.PI) +
        (x >= hC ? 90 : 270)
      );
    }
  }

  handleMeasure = (ox: number, oy: number, width: number, height: number, px: number, py: number) => {
    this.setState({
      xCenter: px + (this.props.dialRadius + this.props.btnRadius),
      yCenter: py + (this.props.dialRadius + this.props.btnRadius)
    });
  }

  handleOnLayout = () => {
    this.circleSlider.measure(this.handleMeasure);
  }

  render () {
    let {
      btnRadius,
      dialRadius,
      rotationOffset,
      textSize
    } = this.props;
    let width = (dialRadius + btnRadius) * 2;
    let startCoord = this.polarToCartesian(this.props.startCoord);
    let endCoord = this.polarToCartesian(this.state.angle);
    let maxAngle = this.polarToCartesian(this.props.maxAngle);

    return (
      <View
        ref={r => this.container = r }
        style={{ flex: 1, margin: 50, transform: [{ rotate: `${rotationOffset}deg` }] }}
      >
        <Svg
          onLayout={this.handleOnLayout}
          ref={r => this.circleSlider = r}
          width={width}
          height={width}
        >
          <Defs>
            <LinearGradient id='gradient1' x1='0%' y1='0%' x2='100%' y2='0%'>
              <Stop offset='0%' stopColor={this.props.startGradient} />
              <Stop offset='100%' stopColor={this.props.endGradient} />
            </LinearGradient>
          </Defs>
          <Text
              transform={`rotate(${-rotationOffset}, 90, 90)`} // compensate for rotated container
              x={width / 2}
              y={width / 2 + textSize / 4}
              fontSize={textSize}
              fill={this.props.textColor}
              textAnchor='middle'
            >
              {this.props.showValue &&
                this.props.onValueChange(this.state.angle) + ''}
            </Text>
          <G>
            <Path
              stroke={this.props.backgroundColor}
              strokeWidth={this.props.dialWidth}
              fill='none'
              strokeLinecap={this.props.cap}
              strokeLinejoin='round'
              d={`M${startCoord.x} ${startCoord.y} A ${dialRadius} ${dialRadius} 0 ${
                  (this.props.startCoord + 180) % 360 > (this.props.maxAngle) ? 0 : 1
                } 1 ${maxAngle.x} ${maxAngle.y}`}
            />
            <Path
              stroke={'url(#gradient1)'}
              strokeWidth={this.props.dialWidth}
              fill='none'
              strokeLinecap={this.props.cap}
              strokeLinejoin='round'
              d={`M${startCoord.x} ${startCoord.y} A ${dialRadius} ${dialRadius} 0 ${
                (this.props.startCoord + 180) % 360 > this.state.angle ? 0 : 1
              } 1 ${endCoord.x} ${endCoord.y}`}
            />
            <G x={endCoord.x - btnRadius} y={endCoord.y - btnRadius}>
              <Circle
                r={btnRadius}
                cx={btnRadius}
                cy={btnRadius}
                fill={this.props.btnFill}
                {...this._panResponder.panHandlers}
              />
            </G>
          </G>
        </Svg>
      </View>
    );
  }
}

目前该组件是这样使用的: <CircularSlider rotationOffset={-135} maxAngle={270} onValueChange={(angle) => angle } /> (或者没有第一个 gif 示例的偏移量)。

我已经尝试过:

  • 在插入 PolarToCartesian() 函数之前调整当前 Angular ,但这变得非常困惑并且没有任何意义,至少我是这样做的。
  • 使用 this.container.measure(this.handleMeasure) 进行实验而不是this.circleSlider.measure(this.handleMeasure) ,和this.circleSlider.measureLayout(ReactNative.findNodeHandle(this.container), this.handleMeasure)并删除额外的参数px: number, py: number从handleMeasure并读取偏移量(ox和oy),但这根本没有给出正确的运动类型......

那么如何在触摸处理中补偿容器 View 的旋转?或者是否有一种完全不同的方法来解决这个问题,为我提供我需要的行为?

(顺便使用React Native 0.57.0和react-native-svg ^7.0.2)

最佳答案

我在onPanResponderMove()函数中调整cartesianToPolar()函数的 Angular 是正确的,我只是没有考虑正确处理maxAngle,大约0。所以在onPanResponderMove()中我需要这个:

let a2 = 0;
if (a + this.props.rotationOffset > 0) {
  a2 = a - (360 + this.props.rotationOffset);
} else {
  a2 = a - this.props.rotationOffset;
}

this.setState({ angle: a2 > 0 ? Math.min(a2, this.props.maxAngle) : this.props.maxAngle });

我还从 View 中删除了旋转变换,并在组中旋转了我的弧路径:

<G transform={`rotate(${this.props.rotationOffset} ${width / 2} ${width / 2})`}>
... my paths ... 
</G>

关于javascript - 如何补偿圆形 slider 组件中的旋转容器 View ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52457264/

相关文章:

javascript - 确定调用 jquery/javascript 函数的元素的当前 ID

javascript - 为链接到文本的图像模式调整 JQuery 代码

javascript - 如何处理javascript中的点击事件?

angular - RXJS 'never' 主题为泛型

javascript - 使用 1 个数组条件和外部 bool 条件过滤数组

javascript - 光滑的 slider - 幻灯片之间的边框

javascript - 为 Angular 模板元素引用赋值的目的是什么?

javascript - 在 android 中 React Native ScrollView 不工作

javascript - 如何在 React Navigation 中禁用向后滑动选项或弹出路线?

reactjs - react useState() & useContext() - TypeError : setState is not a function - What am I doing wrong?