React实现滑动选择插件(仿Antd-mobile Picker)
效果图
需求由于移动端iOS和安卓原生select样式和效果不同,同一个控件在不同系统上效果不同。 Step1 组件分析经过查看和分析后 可以得出结论(如下图)
该组件(Picker)大致分成3个部分
第3部分PickerView即为极为复杂,考虑到扩展性:
分析之后可以看出 第3部分是该组件的核心应该优先制作。 Step 2 使用方法确定在做之前应该想好输入和输出。
参数确定之后要确定两个核心参数的数据结构 const areaArray = [ {label: '北京市',value: '北京市',children: [ {label: '北京市',children: [ {label: '朝阳区',value: '朝阳区'},{label: '海淀区',{label: '东城区',{label: '西城区',value: '朝阳区'} ]} ]},{label: '辽宁省',value: '辽宁省',children: [ {label: '沈阳市',value: '沈阳市',children: [ {label: '沈河区',value: '沈河区'},{label: '浑南区',value: '浑南区'},{label: '沈北新区',value: '沈北新区'},]},{label: '本溪市',value: '本溪市',children: [ {label: '溪湖区',value: '溪湖区'},{label: '东明区',value: '东明区'},{label: '桓仁满族自治县',value: '桓仁满族自治县'},]} ]},{label: '云南省',value: '云南省',children: [ {label: '昆明市',value: '昆明市',children:[ {label: '五华区',value: '五华区'},{label: '官渡区',value: '官渡区'},{label: '呈贡区',value: '呈贡区'},]; 对应value的数据结构: const numberArray = [ [ {label: '一',value: '一'},{label: '二',value: '二'},{label: '三',value: '三'} ],[ {label: '1',value: '1'},{label: '2',value: '2'},{label: '3',value: '3'},{label: '4',value: '4'} ],[ {label: '壹',value: '壹'},{label: '貮',value: '貮'},{label: '叁',value: '叁'} ] ]; 此时value为: Step 3 PickerView制作Picker组件的核心就是PickerView组件 Step 3-1 PickerView搭建PickerView主要的功能就是根据传给自己的props,整理出需要渲染几列PickerColumn,并且整理出PickerColumn需要的参数和回调。
import React from 'react' import PickerColumn from './PickerColumn' // 选择器组件 class PickerView extends React.Component { static defaultProps = { col: 1,cascade: true }; static propTypes = { col: React.PropTypes.number,data: React.PropTypes.array,value: React.PropTypes.array,cascade: React.PropTypes.bool,onChange: React.PropTypes.func }; constructor (props) { super(props); this.state = { defaultSelectedValue: [] } } componentDidMount () { // picker view 当做一个非受控组件 let {value} = this.props; this.setState({ defaultSelectedValue: value }); } handleValueChange (newValue,index) { // 子组件column发生变化的回调函数 // 每次值发生变化 都要判断整个值数组的新值 let {defaultSelectedValue} = this.state; let {data,cascade,onChange} = this.props; let oldValue = defaultSelectedValue.slice(); oldValue[index] = newValue; if(cascade){ // 如果级联的情况下 const newState = this.getNewValue(data,oldValue,[],0); this.setState({ defaultSelectedValue: newState }); // 如果有回调 if(onChange){ onChange(newState); } } else { // 不级联 单纯改对应数据 this.setState({ defaultSelectedValue: oldValue }); // 如果有回调 if(onChange){ onChange(oldValue); } } } getColumns () { let result = []; let {col,data,cascade} = this.props; let {defaultSelectedValue} = this.state; if(defaultSelectedValue.length == 0) return; let array; if(cascade){ array = this.getColumnsData(data,defaultSelectedValue,0); } else { array = data; } for(let i = 0; i < col; i++){ result.push(<PickerColumn key={i} value={defaultSelectedValue[i]} data={array[i]} index={i} onValueChange={this.handleValueChange.bind(this)} />); } return result; } getColumnsData (tree,value,hasFind,deep) { // 遍历tree let has; let array = []; for(let i = 0; i < tree.length; i++){ array.push({label: tree[i].label,value: tree[i].value}); if(tree[i].value == value[deep]) { has = i; } } // 判断有没有找到 // 没找到return // 找到了 没有下一集 也return // 有下一级 则递归 if(has == undefined) return hasFind; hasFind.push(array); if(tree[has].children) { this.getColumnsData(tree[has].children,deep+1); } return hasFind; } getNewValue (tree,newValue,deep) { // 遍历tree let has; for(let i = 0; i < tree.length; i++){ if(tree[i].value == oldValue[deep]) { newValue.push(tree[i].value); has = i; } } if(has == undefined) { has = 0; newValue.push(tree[has].value); } if(tree[has].children) { this.getNewValue(tree[has].children,deep+1); } return newValue; } render () { const columns = this.getColumns(); return ( <div className="zby-picker-view-box"> {columns} </div> ) } } export default PickerView Step 3-2 PickerColumn封装PickerColumn是PickerView的核心,其作用:
这里前两项都好做,关键是3 4两项 选好了插件之后 问题就简单了很多 PickerColumn也就没什么难度了 import React from 'react' import ZScroller from 'zscroller' import classNames from 'classnames' // picker-view 中的列 class PickerColumn extends React.Component { static propTypes = { index: React.PropTypes.number,value: React.PropTypes.string,onValueChange: React.PropTypes.func }; componentDidMount () { // 绑定事件 this.bindScrollEvent(); // 列表滚到对应位置 this.scrollToPosition(); } componentDidUpdate() { this.zscroller.reflow(); this.scrollToPosition(); } componentWillUnmount() { this.zscroller.destroy(); } bindScrollEvent () { // 绑定滚动的事件 const content = this.refs.content; // getBoundingClientRect js原生方法 this.itemHeight = this.refs.indicator.getBoundingClientRect().height; // 最后还是用了何一鸣的zscroll插件 // 但是这个插件并没有太多的文档介绍 gg // 插件demo地址:http://yiminghe.me/zscroller/examples/demo.html let t = this; this.zscroller = new ZScroller(content,{ scrollbars: false,scrollingX: false,snapping: true,// 滚动结束之后 滑动对应的位置 penetrationDeceleration: .1,minVelocityToKeepDecelerating: 0.5,scrollingComplete () { // 滚动结束 回调 t.scrollingComplete(); } }); // 设置每个格子的高度 这样滚动结束 自动滚到对应格子上 // 单位必须是px 所以要动态取一下 this.zscroller.scroller.setSnapSize(0,this.itemHeight); } scrollingComplete () { // 滚动结束 判断当前选中值 const { top } = this.zscroller.scroller.getValues(); const {data,index,onValueChange} = this.props; let currentIndex = top / this.itemHeight; const floor = Math.floor(currentIndex); if (currentIndex - floor > 0.5) { currentIndex = floor + 1; } else { currentIndex = floor; } const selectedValue = data[currentIndex].value; if(selectedValue != value){ // 值发生变化 通知父组件 onValueChange(selectedValue,index); } } scrollToPosition () { // 滚动到选中的位置 let {data,value} = this.props; data.map((item)=>{ if(item.value == value){ this.selectByIndex(); return; } }); for(let i = 0; i < data.length; i++){ if(data[i].value == value){ this.selectByIndex(i); return; } } this.selectByIndex(0); } selectByIndex (index) { // 滚动到index对应的位置 let top = this.itemHeight * index; this.zscroller.scroller.scrollTo(0,top); } getCols () { // 根据value 和 index 获取到对应的data let {data,index} = this.props; let result = []; for(let i = 0; i < data.length; i++){ result.push(<div key={index + "-" + i} className={classNames(['zby-picker-view-col',{'selected': data[i].value == value}])}>{data[i].label}</div>); } return result; } render () { let cols = this.getCols(); return ( <div className="zby-picker-view-item"> <div className="zby-picker-view-list"> <div className="zby-picker-view-window"></div> <div className="zby-picker-view-indicator" ref="indicator"></div> <div className="zby-picker-view-content" ref="content"> {cols} </div> </div> </div> ) } } export default PickerColumn; 这里还有一点要注意,就是CSS Step 4 Picker制作剩下的Picker功能就是很常规的业务了 这里有一点:考虑到页面如果有大量的Picker组件,会产生很多,隐藏的popup和mask,而且每个PickerColumn都要初始化zscroller性能不是很好。所以当没有点击picker的时候mask和popup都是不输出在页面内的; import React from 'react' import classNames from 'classnames' import PickerView from './PickerView' import Touchable from 'rc-touchable' // 选择器组件 class Picker extends React.Component { static defaultProps = { col: 1,cancelText: "取消",confirmText: "确定",cancelText: React.PropTypes.string,title: React.PropTypes.string,confirmText: React.PropTypes.string,onChange: React.PropTypes.func,onCancel: React.PropTypes.func }; constructor (props) { super(props); this.state = { defaultValue: undefined,selectedValue: undefined,animation: "out",show: false } } componentDidMount () { // picker 当做一个非受控组件 let {value} = this.props; this.setState({ defaultValue: value,selectedValue: value }); } handleClickOpen (e) { if(e) e.preventDefault(); this.setState({ show: true }); let t = this; let timer = setTimeout(()=>{ t.setState({ animation: "in" }); clearTimeout(timer); },0); } handleClickClose (e) { if(e) e.preventDefault(); this.setState({ animation: "out" }); let t = this; let timer = setTimeout(()=>{ t.setState({ show: false }); clearTimeout(timer); },300); } handlePickerViewChange (newValue) { let {onPickerChange} = this.props; this.setState({ defaultValue: newValue }); if(onPickerChange){ onPickerChange(newValue); } } handleCancel () { const {defaultValue} = this.state; const {onCancel} = this.props; this.handleClickClose(); this.setState({ selectedValue: defaultValue }); if(onCancel){ onCancel(); } } handleConfirm () { // 点击确认之后的回调 const {defaultValue} = this.state; this.handleClickClose(); if (this.props.onChange) this.props.onChange(defaultValue); } getPopupDOM () { const {show,animation} = this.state; const {cancelText,title,confirmText} = this.props; const pickerViewDOM = this.getPickerView(); if(show){ return <div> <Touchable onPress={this.handleCancel.bind(this)}> <div className={classNames(['zby-picker-popup-mask',{'hide': animation == "out"}])}></div> </Touchable> <div className={classNames(['zby-picker-popup-wrap',{'popup': animation == "in"}])}> <div className="zby-picker-popup-header"> <Touchable onPress={this.handleCancel.bind(this)}> <span className="zby-picker-popup-item zby-header-left">{cancelText}</span> </Touchable> <span className="zby-picker-popup-item zby-header-title">{title}</span> <Touchable onPress={this.handleConfirm.bind(this)}> <span className="zby-picker-popup-item zby-header-right">{confirmText}</span> </Touchable> </div> <div className="zby-picker-popup-body"> {pickerViewDOM} </div> </div> </div> } } getPickerView () { const {col,cascade} = this.props; const {defaultValue,show} = this.state; if(defaultValue != undefined && show){ return <PickerView col={col} data={data} value={defaultValue} cascade={cascade} onChange={this.handlePickerViewChange.bind(this)}> </PickerView>; } } render () { const popupDOM = this.getPopupDOM(); return ( <div className="zby-picker-box"> {popupDOM} <Touchable onPress={this.handleClickOpen.bind(this)}> {this.props.children} </Touchable> </div> ) } } export default Picker 总结Picker到这就结束了,还可以添加一些功能,比如禁止选择的项等。 最后项目源码,Antd-Mobile (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |