React Native-自定义组件之Slider

一、背景

最近在进行原生模块改造RN的时候需要用到一个定制的可拖动进度条,但发现react-native自带的Slider仅仅是在iOS平台上支持,所以决定自己来定制一个。

二、设计思路

组合基础组件和View和Image,搭配PanResponder进行手势监听即可完成。

三、定制方法,分为如下几步:

1.声明属性

首先确定该组件暴露给使用者的属性,以下代码中定义的很多属性基本能满足大部分的对可拖动进度条的差异化需求。

static propTypes = {
        height: PropTypes.number, // 控件高度
        width: PropTypes.number,  // 控件宽度
        maximumTrackTintColor: PropTypes.string, // 进度条背景颜色
        minimumTrackTintColor: PropTypes.string, // 进度条进度部分颜色
        onChange: PropTypes.func, // 进度值发生改变时的回调
        onAfterChange: PropTypes.func, // 拖动结束时的回调
        defaultValue: PropTypes.number, // 默认的进度值
        min: PropTypes.number.isRequired, // 进度范围最小值
        max: PropTypes.number.isRequired, // 进度范围最大值
        step: PropTypes.number.isRequired, // 步长(进度变化的最小单位)
        disabled: PropTypes.bool, // 是否可以拖动
        thumbSize: PropTypes.number, // 滑块的尺寸
        thumbImage: PropTypes.number, // 滑块的图片
        processHeight: PropTypes.number, // 进度条高度
    };

static defaultProps = {
    height: 60,
    width: ScreenWidth,
    onChange: () => {},
    onAfterChange: () => {},
    defaultValue: 0,
    disabled: false,
    thumbSize: 30,
    thumbImage: null,
    maximumTrackTintColor: '#dcdbdb',
    minimumTrackTintColor: '#577BFF',
    processHeight: 7,
  };

2.编写进度条

以下是render方法,state中的process将决定进度条所在的位置,使用两个View填充整个布局(flexDirection: row),左边代表进度值:宽度为process * processWidth,右边代表剩余值:宽度为(1-process) * processWidth。随着process值得改变,这两个View的宽度也将发送改变,从而达到进度值变化的UI效果。

render() {
    const {
        height,
        width,
        maximumTrackTintColor,
        minimumTrackTintColor,
        thumbSize,
        processHeight,
    } = this.props;
    const { process, processWidth } = this.state;

    return (
        <View
            style={[
                styles.container,
                {
                    height,
                    width,
                },
            ]}
            {...this.watcher.panHandlers}
        >
            <View
                style={{
                    backgroundColor: minimumTrackTintColor,
                    width: process * processWidth,
                    height: processHeight,
                    marginLeft: thumbSize / 2,
                }}
            />

            <View
                style={{
                    backgroundColor: maximumTrackTintColor,
                    flex: 1,
                    height: processHeight,
                    marginRight: thumbSize / 2,
                }}
            />

            {this._getThumbView()}
        </View>
    );

3.编写进度条滑块

有了进度条布局,只需要在进度值上放上一个滑块,这里使用绝对布局定位,也是通过process和processWidth计算出当前进度所在的位置,当外部没有传入thumbImage,使用默认的圆形View。

_getThumbView() {
      const { thumbImage, thumbSize } = this.props;
      const { process, processWidth } = this.state;

      if (thumbImage) {
          return (
              <Image
                  style={{
                      width: thumbSize,
                      height: thumbSize,
                      position: 'absolute',
                      left: process * processWidth,
                  }}
                  source={thumbImage}
              />
          );
      }

      return (
          <View
              style={{
                  width: thumbSize,
                  height: thumbSize,
                  position: 'absolute',
                  left: process * processWidth,
                  borderRadius: thumbSize / 2,
                  backgroundColor: '#808080',
              }}
          />
      );
    }

4.添加触摸事件监视器(需了解PanResponder的使用)

监听最外层布局的触摸事件,主要由开始(_onPanResponderGrant)、移动(_onPanResponderEnd)、结束(_onPanResponderMove)个监听函数,在函数中可以知道本次手势坐标所对应的进度比例,统一调用_changeProcess方法来改变进度值,在_changeProcess方法中还会是否可以拖动,通过step来控制进度条拖动的最小单位。

constructor(props) {
      super(props);
      this._onPanResponderGrant = this._onPanResponderGrant.bind(this);
      this._onPanResponderEnd = this._onPanResponderEnd.bind(this);
      this._onPanResponderMove = this._onPanResponderMove.bind(this);
  }

componentWillMount() {
    this.watcher = PanResponder.create({ // 建立监视器
      onStartShouldSetPanResponder: () => true,
      onPanResponderGrant: this._onPanResponderGrant, // 按下
      onPanResponderMove: this._onPanResponderMove, // 移动
      onPanResponderEnd: this._onPanResponderEnd, // 结束
    });
    const {
        defaultValue, min, max, thumbSize,
    } = this.props;
    const process = defaultValue / (max - min);

    this.setState({
        process,
        processWidth: ScreenWidth - thumbSize,
    });
  }

_onPanResponderGrant(e, gestureState) {
        const { thumbSize } = this.props;
        const { processWidth } = this.state;

        const process = (gestureState.x0 - thumbSize / 2) / processWidth;

        this._changeProcess(process);
   }

_onPanResponderEnd(e, gestureState) {
        const { onAfterChange } = this.props;

        if (onAfterChange) {
            onAfterChange(gestureState.x0);
        }
    }

_changeProcess(changeProcess) {
        // 判断滑动开关
        const { disabled } = this.props;

        if (disabled) return;

        const {
            min, max, step, onChange,
        } = this.props;
        const { process } = this.state;

        if (changeProcess >= 0 && changeProcess <= 1) {
            onChange(changeProcess);

            // 按步长比例变化刻度
            const v = changeProcess * (max - min);
            const newValue = Math.round(v / step) * step;
            const newProcess = newValue / (max - min);

            if (process !== newProcess) {
                this.setState({
                    process: newProcess,
                });
            }
        }
    }

四、使用方式

import MySlider form '../components/MySlider';

<MySlider
    height={40}
    min{1000}
    max={200000}
    defaultValue={100000}
    step={1000}
    onChange{() => {}}
    maximumTrackTintColor="#dcdbdb"
    minimumTrackTintColor="#577bff"
    processHeight={5}
    thumbImage={require('../imgs/thumb.png')} />

效果如下:

image.png

五、总结

所有复杂组件都是由基础组件组成的,了解它们的原理后就会发现自定义组件的乐趣。当然为了满足组件的通用性和定制性,还需要将可变部分抽出,同时将组件进行合理的封装,这样才是一个比较完美的自定义组件。查看源码点击这里

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

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,397评论 0 17
  • 记得高中的时候,我们的校训是“播下良好习惯,收获辉煌人生”,所谓习惯即指积久养成的生活方式。养成好的习惯可以无形...
    程修书阅读 365评论 0 0
  • 这次旅行在我过了而立之年生日的第二天出发。 这样的巧合是我当时定下行程时没有计划在内的。 时间上虽是巧合,但与我的...
    莫玄斐隐阅读 385评论 0 1
  • 真是中了蒋勋老师的预言 看过红楼的人肯定会回看
    烟雨江南秀阅读 203评论 0 0
  • 小静一个人走在夜路上。 尽管在城市的街上,车来人往,不显夜的狰狞和冷清。但是走在三三两两的情侣堆里,还是感到些许落...
    老起阅读 184评论 0 2