用 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 內移動時,就把手指劃過的相對距離告知監聽器。
假設手指從 (X1,Y1)
點滑到 (X2,Y2)
點,那麼手指在兩點間滑動的X軸相對距離就是 X2 - X1
,Y軸相對距離 Y2 - Y1
。所以,只要我們能夠獲取手指的座標位置,就能算出手指每次移動的相對距離,然後把ΔX和ΔY告知 move 事件的監聽函式。 所以,move事件的監聽器一般是這樣(注意ES6語法):
_onMove (event) { let { deltaX, //手指在X軸上的位移 deltaY //手指在Y軸上的位移 } = event; ... }
無論多麼複雜的手勢系統,他們都會基於四個最基礎的觸控事件:
- touchstart
- touchmove
- touchend
- touchcancel
通過他們可以獲取手指觸控點的座標資訊,進而算出手指移動的相對距離。
根據上面的圖解,先來實現 touch 事件監聽函式:
_onTouchStart(e) { let point = e.touches ? e.touches[0] : e; this.startX= point.pageX; this.startY = point.pageY; }
_onTouchStart
函式非常簡單,就是記錄下初始觸控點的座標,儲存在startX startY
變數中。
_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(); }
_onTouchMove
函式邏輯也比較清楚,通過 touch 的觸控點 point
和 startX, startY
得到手指的相對位移 deltaX, deltaY
, 然後發出 onMove
事件,告知監聽器有 move 事件發生,並攜帶deltaX, deltaY
資訊。最後,用現在的觸控點座標去更新 startX, startY
。
_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 值。即:
translateX = deltaX0 + deltaX1 + … + deltaXn
有了這個公式,就可以用 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 中,用 this.onMove
去監聽 move 事件。在 onMove 函式中,需要累加 deltaX 作為 toggler 的位移。
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 }); }
注意this.xBoundary
,toggler 不能無限制的移動,必須限制在 wrapper 的範圍內,這個範圍的下限是0,上限是 wrapper 的寬度減去 toggler 的寬度。
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 版本就完成了,點選這裡線上檢視你的大作吧。
然而還有兩個明顯的問題。
- 現在只要手指進入 wrapper 的範圍,就可以滑動 toggler 了。而我們的需求是隻有當手指進入 toggler 才能滑動。
- 當手指抬起時,toggler 就立即停止移動了。而我們的需求是當手指抬起時,toggler 需要自動滑到開始或者結束的位置。
也就是說,還需要監聽手指在 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
相關文章
- React Native 互動元件之 SwitchReact Native元件
- React元件複用的方式React元件
- React元件及應用React元件
- REACT元件抽象與複用React元件抽象
- 開發 react 應用最好用的腳手架 create-react-appReactAPP
- react 高階元件的 理解和應用React元件
- switch button 待完善,做出一個合理的開關按鈕
- angular自定義元件-UI元件篇-switch元件Angular元件UI
- React元件化複用的一些技巧React元件化
- react高階元件的一些運用React元件
- react篇章-React 元件-複合元件React元件
- react元件的建立React元件
- React的元件模式React元件模式
- 自定義Switch控制元件控制元件
- 用Jest和Enzyme測試React元件React元件
- 簡述用React實現Table元件React元件
- React元件應用於Spring MVC工程React元件SpringMVC
- React篇章-React 元件React元件
- react中元件之間的資料傳送,交流,最簡單,通用,好用的方法.React元件
- React 中的高階元件及其應用場景React元件
- React中元件邏輯複用的那些事兒React元件
- (翻譯)用react-spring以react hook元件的形式編寫動畫ReactSpringHook元件動畫
- React元件React元件
- react hooks 如何自定義元件(react函式元件的封裝)ReactHook元件函式封裝
- React的非同步元件React非同步元件
- 微前端(qiankun)主應用共享React元件前端React元件
- Flutter控制元件--Switch 和 SwitchListTileFlutter控制元件
- React 實現一個簡單實用的 Form 元件ReactORM元件
- 10 個好用的自定義 React HooksReactHook
- 安利:vue 好用的拖拽排序元件Vue排序元件
- Android 好用的自定義元件、框架Android元件框架
- react篇章-React 元件-向元件傳遞引數React元件
- [譯] React 中的 dumb 元件和 smart 元件React元件
- react元件(react-grid-gallery)React元件
- 用 SOLID 原則保駕 React 元件開發SolidReact元件
- react複合元件的使用React元件
- React高階元件的使用React元件
- 說說React元件的StateReact元件