react-native 圆弧拖动进度条实现

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

先上效果图

[图片上传失败...(image-84532f-1523241227121)]

因为需求需要实现这个效果图 非原生实现,

  • 难点1:绘制 使用svg
  • 难点2:点击事件的处理
  • 难点3:封装

由于绘制需要是使用svg

此处自行百度 按照svg以及api 教学

视图代码块


 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>
    );


 _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
    );
    // 根据象限做处理 苦苦苦 高中数学全忘了,参考辅助线
    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,
      //根据角度是否是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 官网对手势的讲解
 iniPanResponder() {
    this.parseToDeg = this.parseToDeg.bind(this);
    this._panResponder = PanResponder.create({
      // 要求成为响应者:
      onStartShouldSetPanResponder: () => true,
      onStartShouldSetPanResponderCapture: () => true,
      onMoveShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponderCapture: () => true,
      onPanResponderGrant: evt => {
        // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
        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);
      },
      // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
      onPanResponderTerminate: () => {},
      // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
      // 默认返回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;
    }
    //因为提供步长,所欲需要做接近步长的数
    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({
      // 要求成为响应者:
      onStartShouldSetPanResponder: () => true,
      onStartShouldSetPanResponderCapture: () => true,
      onMoveShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponderCapture: () => true,
      onPanResponderGrant: evt => {
        // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
        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);
      },
      // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
      onPanResponderTerminate: () => {},
      // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
      // 默认返回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
    );
    // 根据象限做处理 苦苦苦 高中数学全忘了,参考辅助线
    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: {},
});

外部调用

<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>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,498评论 25 707
  • 先知:女士们、先生们:欢迎来到生命之泉,大家尽情享受泉水的温暖,感受生命的乐趣!世界虽已和平,但环境不容乐观,人们...
    壶上春秋阅读 340评论 0 0
  • 而另一旁,常言胜站了起来。 他头都不会就走了。 众弟子没有放在心上,以为是常言胜一时没从失败的打击里走出来,毕竟,...
    总有宫女想非礼朕阅读 192评论 0 2
  • 考试的座位是按上一次的名次排的,所以他正好又与林渺隔一个走道。他做完题目看她时,只看见一对惺忪的眼睛在桌子上似望非...
    何青猊阅读 118评论 0 2
  • 1,烦工作 投了若干份简历,要么被判为不合适,要么杳无音信。想要转行,但是隔行如隔山,一窍不通。没有认识的人愿意带...
    小瘾吖阅读 579评论 0 0