react-native 圓弧拖動進度條實現(xiàn)

原文鏈接:https://blog.csdn.net/qq_22329521/article/details/79862355

先上效果圖

[圖片上傳失敗...(image-84532f-1523241227121)]

因為需求需要實現(xiàn)這個效果圖 非原生實現(xiàn),

  • 難點1:繪制 使用svg
  • 難點2:點擊事件的處理
  • 難點3:封裝

由于繪制需要是使用svg

此處自行百度 按照svg以及api 教學

視圖代碼塊


 render() {
    return (
      <View pointerEvents={'box-only'}
      //事件處理
       {...this._panResponder.panHandlers}>
       //實際圓環(huán)
        {this._renderCircleSvg()}
        // 計算中心距離
        <View
          style={{
            position: 'relative',
            top: -this.props.height / 2 - this.props.r,
            left: this.props.width / 2 - this.props.r,
            flex: 1,
          }}>
          // 暴露給外部渲染圓環(huán)中心的接口
          {this.props.renderCenterView(this.state.temp)}
        </View>
      </View>
    );


 _renderCircleSvg() {
   //中心點
    const cx = this.props.width / 2;
    const cy = this.props.height / 2;
    //計算是否有偏差角 對應(yīng)圖就是下面缺了一塊的
    const prad = this.props.angle / 2 * (Math.PI / 180);
    //三角計算起點
    const startX = -(Math.sin(prad) * this.props.r) + cx;
    const startY = cy + Math.cos(prad) * this.props.r; 
    //終點
    const endX = Math.sin(prad) * this.props.r + cx;
    const endY = cy + Math.cos(prad) * this.props.r;

    // 計算進度點
    const progress = parseInt(
      this._circlerate() * (360 - this.props.angle) / 100,
      10
    );
    // 根據(jù)象限做處理 苦苦苦 高中數(shù)學全忘了,參考輔助線
    const t = progress + this.props.angle / 2;
    const progressX = cx - Math.sin(t * (Math.PI / 180)) * this.props.r;
    const progressY = cy + Math.cos(t * (Math.PI / 180)) * this.props.r;

// SVG的描述 這里百度下就知道什么意思
    const descriptions = [
      'M',
      startX,
      startY,
      'A',
      this.props.r,
      this.props.r,
      0,
      1,
      1,
      endX,
      endY,
    ].join(' ');

    const progressdescription = [
      'M',
      startX,
      startY,
      'A',
      this.props.r,
      this.props.r,
      0,
      //根據(jù)角度是否是0,1 看下效果就知道了
      t >= 180 + this.props.angle / 2 ? 1 : 0,
      1,
      progressX,
      progressY,
    ].join(' ');
    return (
      <Svg
        height={this.props.height}
        width={this.props.width}
        style={styles.svg}>
        <Path
          d={descriptions}
          fill="none"
          stroke={this.props.outArcColor}
          strokeWidth={this.props.strokeWidth} />
        <Path
          d={progressdescription}
          fill="none"
          stroke={this.props.progressvalue}
          strokeWidth={this.props.strokeWidth} />
        <Circle
          cx={progressX}
          cy={progressY}
          r={this.props.tabR}
          stroke={this.props.tabStrokeColor}
          strokeWidth={this.props.tabStrokeWidth}
          fill={this.props.tabColor} />
      </Svg>
    );
  }
}

事件處理代碼塊

// 參考react native 官網(wǎng)對手勢的講解
 iniPanResponder() {
    this.parseToDeg = this.parseToDeg.bind(this);
    this._panResponder = PanResponder.create({
      // 要求成為響應(yīng)者:
      onStartShouldSetPanResponder: () => true,
      onStartShouldSetPanResponderCapture: () => true,
      onMoveShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponderCapture: () => true,
      onPanResponderGrant: evt => {
        // 開始手勢操作。給用戶一些視覺反饋,讓他們知道發(fā)生了什么事情!
        if (this.props.enTouch) {
          this.lastTemper = this.state.temp;
          const x = evt.nativeEvent.locationX;
          const y = evt.nativeEvent.locationY;
          this.parseToDeg(x, y);
        }
      },
      onPanResponderMove: (evt, gestureState) => {
        if (this.props.enTouch) {
          let x = evt.nativeEvent.locationX;
          let y = evt.nativeEvent.locationY;
          if (Platform.OS === 'android') {
            x = evt.nativeEvent.locationX + gestureState.dx;
            y = evt.nativeEvent.locationY + gestureState.dy;
          }
          this.parseToDeg(x, y);
        }
      },
      onPanResponderTerminationRequest: () => true,
      onPanResponderRelease: () => {
        if (this.props.enTouch) this.props.complete(this.state.temp);
      },
      // 另一個組件已經(jīng)成為了新的響應(yīng)者,所以當前手勢將被取消。
      onPanResponderTerminate: () => {},
      // 返回一個布爾值,決定當前組件是否應(yīng)該阻止原生組件成為JS響應(yīng)者
      // 默認返回true。目前暫時只支持android。
      onShouldBlockNativeResponder: () => true,
    });
  }

//畫象限看看就知道了 就是和中線點計算角度
parseToDeg(x, y) {
    const cx = this.props.width / 2;
    const cy = this.props.height / 2;
    let deg;
    let temp;
    if (x >= cx && y <= cy) {
      deg = Math.atan((cy - y) / (x - cx)) * 180 / Math.PI;
      temp =
        (270 - deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x >= cx && y >= cy) {
      deg = Math.atan((cy - y) / (cx - x)) * 180 / Math.PI;
      temp =
        (270 + deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x <= cx && y <= cy) {
      deg = Math.atan((x - cx) / (y - cy)) * 180 / Math.PI;
      temp =
        (180 - this.props.angle / 2 - deg) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x <= cx && y >= cy) {
      deg = Math.atan((cx - x) / (y - cy)) * 180 / Math.PI;
      if (deg < this.props.angle / 2) {
        deg = this.props.angle / 2;
      }
      temp =
        (deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    }
    if (temp <= this.props.min) {
      temp = this.props.min;
    }
    if (temp >= this.props.max) {
      temp = this.props.max;
    }
    //因為提供步長,所欲需要做接近步長的數(shù)
    temp = this.getTemps(temp);
    this.setState({
      temp,
    });
    this.props.valueChange(this.state.temp);
  }

  getTemps(tmps) {
    const k = parseInt((tmps - this.props.min) / this.props.step, 10);
    const k1 = this.props.min + this.props.step * k;
    const k2 = this.props.min + this.props.step * (k + 1);
    if (Math.abs(k1 - tmps) > Math.abs(k2 - tmps)) return k2;
    return k1;
  }

完整代碼塊

import React, { Component } from 'react';
import { View, StyleSheet, PanResponder, Platform, Text } from 'react-native';
import Svg, { Circle, Path } from 'react-native-svg';

export default class CircleView extends Component {
  static propTypes = {
    height: React.PropTypes.number,
    width: React.PropTypes.number,
    r: React.PropTypes.number,
    angle: React.PropTypes.number,
    outArcColor: React.PropTypes.object,
    progressvalue: React.PropTypes.object,
    tabColor: React.PropTypes.object,
    tabStrokeColor: React.PropTypes.object,
    strokeWidth: React.PropTypes.number,
    value: React.PropTypes.number,
    min: React.PropTypes.number,
    max: React.PropTypes.number,
    tabR: React.PropTypes.number,
    step: React.PropTypes.number,
    tabStrokeWidth: React.PropTypes.number,
    valueChange: React.PropTypes.func,
    renderCenterView: React.PropTypes.func,
    complete: React.PropTypes.func,
    enTouch: React.PropTypes.boolean,
  };

  static defaultProps = {
    width: 300,
    height: 300,
    r: 100,
    angle: 60,
    outArcColor: 'white',
    strokeWidth: 10,
    value: 20,
    min: 10,
    max: 70,
    progressvalue: '#ED8D1B',
    tabR: 15,
    tabColor: '#EFE526',
    tabStrokeWidth: 5,
    tabStrokeColor: '#86BA38',
    valueChange: () => {},
    complete: () => {},
    renderCenterView: () => {},
    step: 1,
    enTouch: true,
  };
  constructor(props) {
    super(props);
    this.state = {
      temp: this.props.value,
    };
    this.iniPanResponder();
  }
  iniPanResponder() {
    this.parseToDeg = this.parseToDeg.bind(this);
    this._panResponder = PanResponder.create({
      // 要求成為響應(yīng)者:
      onStartShouldSetPanResponder: () => true,
      onStartShouldSetPanResponderCapture: () => true,
      onMoveShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponderCapture: () => true,
      onPanResponderGrant: evt => {
        // 開始手勢操作。給用戶一些視覺反饋,讓他們知道發(fā)生了什么事情!
        if (this.props.enTouch) {
          this.lastTemper = this.state.temp;
          const x = evt.nativeEvent.locationX;
          const y = evt.nativeEvent.locationY;
          this.parseToDeg(x, y);
        }
      },
      onPanResponderMove: (evt, gestureState) => {
        if (this.props.enTouch) {
          let x = evt.nativeEvent.locationX;
          let y = evt.nativeEvent.locationY;
          if (Platform.OS === 'android') {
            x = evt.nativeEvent.locationX + gestureState.dx;
            y = evt.nativeEvent.locationY + gestureState.dy;
          }
          this.parseToDeg(x, y);
        }
      },
      onPanResponderTerminationRequest: () => true,
      onPanResponderRelease: () => {
        if (this.props.enTouch) this.props.complete(this.state.temp);
      },
      // 另一個組件已經(jīng)成為了新的響應(yīng)者,所以當前手勢將被取消。
      onPanResponderTerminate: () => {},
      // 返回一個布爾值,決定當前組件是否應(yīng)該阻止原生組件成為JS響應(yīng)者
      // 默認返回true。目前暫時只支持android。
      onShouldBlockNativeResponder: () => true,
    });
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.value != this.state.temp) {
      this.state = {
        temp: nextProps.value,
      };
    }
  }
  parseToDeg(x, y) {
    const cx = this.props.width / 2;
    const cy = this.props.height / 2;
    let deg;
    let temp;
    if (x >= cx && y <= cy) {
      deg = Math.atan((cy - y) / (x - cx)) * 180 / Math.PI;
      temp =
        (270 - deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x >= cx && y >= cy) {
      deg = Math.atan((cy - y) / (cx - x)) * 180 / Math.PI;
      temp =
        (270 + deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x <= cx && y <= cy) {
      deg = Math.atan((x - cx) / (y - cy)) * 180 / Math.PI;
      temp =
        (180 - this.props.angle / 2 - deg) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    } else if (x <= cx && y >= cy) {
      deg = Math.atan((cx - x) / (y - cy)) * 180 / Math.PI;
      if (deg < this.props.angle / 2) {
        deg = this.props.angle / 2;
      }
      temp =
        (deg - this.props.angle / 2) /
          (360 - this.props.angle) *
          (this.props.max - this.props.min) +
        this.props.min;
    }
    if (temp <= this.props.min) {
      temp = this.props.min;
    }
    if (temp >= this.props.max) {
      temp = this.props.max;
    }

    temp = this.getTemps(temp);
    this.setState({
      temp,
    });
    this.props.valueChange(this.state.temp);
  }

  getTemps(tmps) {
    const k = parseInt((tmps - this.props.min) / this.props.step, 10);
    const k1 = this.props.min + this.props.step * k;
    const k2 = this.props.min + this.props.step * (k + 1);
    if (Math.abs(k1 - tmps) > Math.abs(k2 - tmps)) return k2;
    return k1;
  }

 
  render() {
    return (
      <View pointerEvents={'box-only'} {...this._panResponder.panHandlers}>
        {this._renderCircleSvg()}
        <View
          style={{
            position: 'relative',
            top: -this.props.height / 2 - this.props.r,
            left: this.props.width / 2 - this.props.r,
            flex: 1,
          }}>
          {this.props.renderCenterView(this.state.temp)}
        </View>
      </View>
    );
  }

  _circlerate() {
    let rate = parseInt(
      (this.state.temp - this.props.min) *
        100 /
        (this.props.max - this.props.min),
      10
    );
    if (rate < 0) {
      rate = 0;
    } else if (rate > 100) {
      rate = 100;
    }
    return rate;
  }
  _renderCircleSvg() {
    const cx = this.props.width / 2;
    const cy = this.props.height / 2;
    const prad = this.props.angle / 2 * (Math.PI / 180);
    const startX = -(Math.sin(prad) * this.props.r) + cx;
    const startY = cy + Math.cos(prad) * this.props.r; // // 最外層的圓弧配置
    const endX = Math.sin(prad) * this.props.r + cx;
    const endY = cy + Math.cos(prad) * this.props.r;

    // 計算進度點
    const progress = parseInt(
      this._circlerate() * (360 - this.props.angle) / 100,
      10
    );
    // 根據(jù)象限做處理 苦苦苦 高中數(shù)學全忘了,參考輔助線
    const t = progress + this.props.angle / 2;
    const progressX = cx - Math.sin(t * (Math.PI / 180)) * this.props.r;
    const progressY = cy + Math.cos(t * (Math.PI / 180)) * this.props.r;

    const descriptions = [
      'M',
      startX,
      startY,
      'A',
      this.props.r,
      this.props.r,
      0,
      1,
      1,
      endX,
      endY,
    ].join(' ');

    const progressdescription = [
      'M',
      startX,
      startY,
      'A',
      this.props.r,
      this.props.r,
      0,
      t >= 180 + this.props.angle / 2 ? 1 : 0,
      1,
      progressX,
      progressY,
    ].join(' ');
    return (
      <Svg
        height={this.props.height}
        width={this.props.width}
        style={styles.svg}>
        <Path
          d={descriptions}
          fill="none"
          stroke={this.props.outArcColor}
          strokeWidth={this.props.strokeWidth} />
        <Path
          d={progressdescription}
          fill="none"
          stroke={this.props.progressvalue}
          strokeWidth={this.props.strokeWidth} />
        <Circle
          cx={progressX}
          cy={progressY}
          r={this.props.tabR}
          stroke={this.props.tabStrokeColor}
          strokeWidth={this.props.tabStrokeWidth}
          fill={this.props.tabColor} />
      </Svg>
    );
  }
}

const styles = StyleSheet.create({
  svg: {},
});

外部調(diào)用

<View style={styles.container}>
        <CircleProgress
          width={width}
          height={height}
          r={r}
          angle={60}
          min={5}
          max={35}
          step={0.5}
          value={22}
          complete={temp => {
           
          }}
          valueChange={temp => {}}
          renderCenterView={temp => (
            <View style={{ flex: 1 }}>
             
            </View>
          )}
          enTouch={true} />
      </View>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,716評論 25 709
  • 先知:女士們、先生們:歡迎來到生命之泉,大家盡情享受泉水的溫暖,感受生命的樂趣!世界雖已和平,但環(huán)境不容樂觀,人們...
    壺上春秋閱讀 417評論 0 0
  • 而另一旁,常言勝站了起來。 他頭都不會就走了。 眾弟子沒有放在心上,以為是常言勝一時沒從失敗的打擊里走出來,畢竟,...
    總有宮女想非禮朕閱讀 217評論 0 2
  • 考試的座位是按上一次的名次排的,所以他正好又與林渺隔一個走道。他做完題目看她時,只看見一對惺忪的眼睛在桌子上似望非...
    何青猊閱讀 150評論 0 2
  • 1,煩工作 投了若干份簡歷,要么被判為不合適,要么杳無音信。想要轉(zhuǎn)行,但是隔行如隔山,一竅不通。沒有認識的人愿意帶...
    小癮吖閱讀 637評論 0 0

友情鏈接更多精彩內(nèi)容