一、背景
最近在进行原生模块改造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 (
{this._getThumbView()}
);
3.编写进度条滑块
有了进度条布局,只需要在进度值上放上一个滑块,这里使用绝对布局定位,也是通过process和processWidth计算出当前进度所在的位置,当外部没有传入thumbImage,使用默认的圆形View。
_getThumbView() {
const { thumbImage, thumbSize } = this.props;
const { process, processWidth } = this.state;
if (thumbImage) {
return (
);
}
return (
);
}
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';
{}}
maximumTrackTintColor="#dcdbdb"
minimumTrackTintColor="#577bff"
processHeight={5}
thumbImage={require('../imgs/thumb.png')} />
效果如下:
五、总结
所有复杂组件都是由基础组件组成的,了解它们的原理后就会发现自定义组件的乐趣。当然为了满足组件的通用性和定制性,还需要将可变部分抽出,同时将组件进行合理的封装,这样才是一个比较完美的自定义组件。查看源码点击这里