用 React 做出好用的 Switch 组件
关于作者周林,github,陆金所前端程序员,专注 Hybrid APP 性能优化和新技术探索。欢迎任何形式的提问和讨论。 前言HTML5 将 WEB 开发者的战场从传统的 PC 端带到了移动端。然而移动端交互的核心在于手势和滑动,如果只是将 PC 端的点击体验简单地移植到移动端,势必让移动端体验变得了无生趣。以某 APP 收银台的支付密码输入框为例,里面的 Switch 组件只能通过点击改变状态,和原生控件的体验有着非常大的差距,不符合移动端的交互习惯。接下来,我们来尝试做出一个支持手指滑动操作的Switch组件,提升用户体验。 手势检测手势交互的关键在于一套手势事件监测系统,用于检测move,tap,double tap,long tap,swipe,pinch,rotate等手势行为。安卓和 IOS 都提供一套完善的手势系统供原生 APP 调用,遗憾的是,HTML5 还没有相应的 API,需要 HTML5 工程师自己实现。出于简化,我们的 Switch 组件只支持 move 事件,因此,本章也只实现 move 事件的检测。其他事件的检测我们将在下一篇博文?HTML5 手势检测原理和实现?中详细介绍。 我们对move事件的要求非常简单,就是每当手指在 DOM 内移动时,就把手指划过的相对距离告知监听器。 假设手指从 _onMove (event) { let { deltaX,//手指在X轴上的位移 deltaY //手指在Y轴上的位移 } = event; ... } 无论多么复杂的手势系统,他们都会基于四个最基础的触摸事件:
通过他们可以获取手指触摸点的坐标信息,进而算出手指移动的相对距离。 根据上面的图解,先来实现 touch 事件监听函数: _onTouchStart(e) { let point = e.touches ? e.touches[0] : e; this.startX= point.pageX; this.startY = point.pageY; }
_onTouchMove(e) { let point = e.touches ? e.touches[0] :e; let deltaX = point.pageX - this.startX; let deltaY = point.pageY - this.startY; this._emitEvent('onMove',{ deltaX,deltaY }); this.startX = point.pageX; this.startY = point.pageY; e.preventDefault(); }
_onTouchEnd(e) { this.startX = 0; this.startY = 0; } _onTouchCancel(e) { this._onTouchEnd(); } 既然我们要用 React 实现组件,那就把 move 事件转化成 React 代码: render() { return React.cloneElement(React.Children.only(this.props.children),{ onTouchStart: this._onTouchStart.bind(this),onTouchMove: this._onTouchMove.bind(this),onTouchCancel: this._onTouchCancel.bind(this),onTouchEnd: this._onTouchEnd.bind(this) }); } 一定注意我们用了React.Children.only限制只有一个子级,思考一下为什么。完整的代码请参考这里,我们只给出大致结构: export default class Gestures extends Component { constructor(props) {} _emitEvent(eventType,e) {} _onTouchStart(e) {} _onTouchMove(e) {} _onTouchCancel(e){} _onTouchEnd(e){} render(){} } Gestures.propTypes = { onMove: PropTypes.func }; Switch 组件实现Switch 组件的 DOM 结构并不复杂,由最外的 wrapper 层包裹里层的 toggler。 有一点要注意,toggler 需要设置为 absolute 定位。因为这样,就可以将手指在 wrapper X轴上的相对滑动距离 deltaX 转化为 toggler 的 tranlate 的 x 值。 render() { return ( <div ref="wrapper" className="wrapper"> <div ref="toggler" className="toggler"></div> </div> ); } 那 move 事件应该加在 wrapper 上面还是 toggler 上面呢?经验之谈,在固定不动的元素上检测手势事件,这会为你减少很多bug。 我们在 wrapper 上监听手指的 move 事件,将 move 事件发出的 deltaX 做累加,就是 toggler 的 translate 的 x 值。即:
有了这个公式,就可以用 React 来实现了。首先修改render函数 render() { let {translateX} = this.state; let toggleStyle = { transform: `translate(${translateX}px,0px) translateZ(0)`,WebkitTransform: `translate(${translateX}px,0px) translateZ(0)` } return ( <Gestures onMove={this.onMove}> <div className="wrapper ref="wrapper" > <div className="toggler" ref="togger" style={toggleStyle}></div> </div> </Gestures>); } 在 Gestures 中,用 onMove(e) { this.translateX += deltaX; if(this.translateX >= this.xBoundary) this.translateX = this.xBoundary; this.translateX = this.translateX <=1 ? 0 : this.translateX; this.setState({ translateX: this.translateX }); } 注意 componentDidMount() { this.xBoundary = ReactDOM.findDOMNode(this.refs.wrapper).clientWidth - ReactDOM.findDOMNode(this.refs.togger).offsetWidth; this.toggerDOM = ReactDOM.findDOMNode(this.refs.togger); this.toggerDOM.translateX = 0; } 好了,这样 Switch 组件的 V1 版本就完成了,点击这里在线查看你的大作吧。 然而还有两个明显的问题。
也就是说,还需要监听手指在 toggler 上面的 touchstart 和 touchend 事件。当 touchstart 发生时,需要打开 toggler 移动的开关,当 touchend 发生时,需要根据情况让 toggler 滑到开始或结束的位置。 逻辑还是很清楚,下面来修改代码吧: 首先为 toggler 加上 touch 监听函数 render() { ... <div className="toggler" onTouchStart={this.onToggerTouchStart} onTouchCancel={this.onToggerTouchCancel} onTouchEnd={this.onToggerTouchCancel} ref="togger" style={toggleStyle}> </div> ... } 在 onToggerTouchStart 函数中,打开滑动开关(movingEnable),同时取消 toggler 位移动画。 onToggerTouchStart(e) { this.movingEnable = true; this.enableTransition(false); } 在 onToggerTouchCancel 函数中,关闭滑动开关,同时为 toggler 添加一个位移动画。还根据 toggler 此时的位移量(translateX),将 toggler 调整为回到初始位置(0) 或者回到最大位置(xBoundary)。 onToggerTouchCancel(e) { this.movingEnable = false; this.enableTransition(true); if(this.translateX < this.xBoundary /2) { this.translateX = 0; }else { this.translateX = this.xBoundary; } this.setState({ translateX: this.translateX,}); } 这样,我们的 Switch组件就大功告成了,在这里在线体验。 完整代码请参考Github (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |