摘要:前言推出已有接近年,大家對(duì)觸摸漣漪應(yīng)該不陌生簡(jiǎn)單來說就是一個(gè)水波紋效果見下圖。現(xiàn)已開源到,以及相應(yīng)的。這樣一來,這些事件都會(huì)發(fā)生在組件上,問題解決。和這個(gè)函數(shù)負(fù)責(zé)計(jì)算事件發(fā)生的坐標(biāo),的大小等信息。就會(huì)構(gòu)建一個(gè),然后將其放入中。
前言
Material Design 推出已有接近4年,大家對(duì)“觸摸漣漪”(Ripple)應(yīng)該不陌生,簡(jiǎn)單來說就是一個(gè)水波紋效果(見下圖)。前段時(shí)間接觸了 material-ui 這個(gè)庫(kù),看了下 Ripple 的源碼,覺得并不是一個(gè)非常好的實(shí)現(xiàn),所以決定自己寫一個(gè) React 組件—— React Touch Ripple。現(xiàn)已開源到 Github,以及相應(yīng)的 Demo。
組件拆分我們把組件拆分為兩個(gè)組件:RippleWrapper 和 Ripple。
Ripple 就是一個(gè)圓形,漣漪本身,它會(huì)接受 rippleX, rippleY 這樣的坐標(biāo)在相應(yīng)位置渲染,以及 rippleSize 決定其大小。
RippleWrapper 是所有 Ripple 的容器,它內(nèi)部會(huì)維護(hù)一個(gè) state: { rippleArray: [] }。
所有的事件監(jiān)聽器也會(huì)綁定在 RippleWrapper 上,每次新增一個(gè) Ripple 就將其 push 進(jìn) rippleArray 中,相應(yīng)地一個(gè) Ripple 消失時(shí)就移除 rippleArray 的第一個(gè)元素。
RippleRipple 這個(gè)組件的實(shí)現(xiàn)比較簡(jiǎn)單,它是一個(gè)純函數(shù)。首先根據(jù) Material Design 的規(guī)范,簡(jiǎn)述下動(dòng)畫渲染過程:
enter 階段:ripple 逐漸擴(kuò)大(transform: scale(0) 到 transform: scale(1)),同時(shí)透明度逐漸增加(opacity: 0 到 opacity: 0.3)。
exit 階段: ripple 消失,這里就不再改變 scale,直接設(shè)置 opacity: 0。
class Ripple extends React.Component { state = { rippleEntering: false, wrapperExiting: false, }; handleEnter = () => { this.setState({ rippleEntering: true, }); } handleExit = () => { this.setState({ wrapperExiting: true, }); } render () { const { className, rippleX, rippleY, rippleSize, color, timeout, ...other } = this.props; const { wrapperExiting, rippleEntering } = this.state; return (); } }
注意這兩個(gè) class:rtr-ripple-entering,rtr-ripple-wrapper-exiting 對(duì)應(yīng)這兩個(gè)動(dòng)畫的樣式。
.rtr-ripple-wrapper-exiting { opacity: 0; animation: rtr-ripple-exit 500ms cubic-bezier(0.4, 0, 0.2, 1); } .rtr-ripple-entering { opacity: 0.3; transform: scale(1); animation: rtr-ripple-enter 500ms cubic-bezier(0.4, 0, 0.2, 1) } @keyframes rtr-ripple-enter { 0% { transform: scale(0); } 100% { transform: scale(1); } } @keyframes rtr-ripple-exit { 0% { opacity: 1; } 100% { opacity: 0; } }
rippleX,rippleY,rippleSize 這些 props,直接設(shè)置 style 即可。
至于這些值是如何計(jì)算的,我們接下來看 RippleWrapper 的實(shí)現(xiàn)。
RippleWrapper這個(gè)組件要做的事情比較多,我們分步來實(shí)現(xiàn)
事件處理首先看 event handler 的部分。
class RippleWrapper extends React.Component { handleMouseDown = (e) => { this.start(e); } handleMouseUp = (e) => { this.stop(e); } handleMouseLeave = (e) => { this.stop(e); } handleTouchStart = (e) => { this.start(e); } handleTouchEnd = (e) => { this.stop(e); } handleTouchMove = (e) => { this.stop(e); } render () {{this.state.rippleArray} } }
這里的 event handler 分為兩部分。對(duì)于 mousedown,touchstart 這兩個(gè)事件,就意味著需要?jiǎng)?chuàng)建一個(gè)新的 Ripple,當(dāng) mouseup,mouseleave,touchend,touchmove 這些事件觸發(fā)時(shí),就意味著這個(gè) Ripple 該被移除了。
注意這里有一個(gè)“巨坑”,那就是快速點(diǎn)擊時(shí),onclick 事件并不會(huì)被觸發(fā)。(見下圖,只輸出了 "mousedown",而沒有 "onclick")
我們知道,Ripple 的主要用處在于 button 組件,雖然我們并不處理 click 事件,但使用者綁定的 onClick 事件依賴于它的冒泡,如果這里不觸發(fā) click 的話用戶就無法處理 button 上的點(diǎn)擊事件了。這個(gè) bug 的產(chǎn)生原因直到我翻到 w3 working draft 才搞清楚。
注意這句話
The click event MAY be preceded by the mousedown and mouseup events on the same element
也就是說,mousedown 和 mouseup 需要發(fā)生在同一節(jié)點(diǎn)上(不包括文本節(jié)點(diǎn)),click 事件才會(huì)被觸發(fā)。所以,當(dāng)我們快速點(diǎn)擊時(shí),mousedown 會(huì)發(fā)生在“上一個(gè)” Ripple 上。當(dāng) mouseup 發(fā)生時(shí),那個(gè) Ripple 已經(jīng)被移除了,它會(huì)發(fā)生在“當(dāng)前”的 Ripple 上,于是 click 事件沒有觸發(fā)。
弄清了原因后,解決方法非常簡(jiǎn)單。我們其實(shí)不需要 Ripple 組件響應(yīng)這些事件,只需要加一行 css:pointer-events: none 即可。這樣一來 mousedown,mouseup 這些事件都會(huì)發(fā)生在 RippleWrapper 組件上,問題解決。
start 和 stopstart 這個(gè)函數(shù)負(fù)責(zé)計(jì)算事件發(fā)生的坐標(biāo),ripple 的大小等信息。注意在計(jì)算坐標(biāo)時(shí),我們需要的是“相對(duì)”坐標(biāo),相對(duì) RippleWrapper 這個(gè)組件來的。而 e.clientX,e.clientY 獲得的坐標(biāo)是相對(duì)整個(gè)頁面的。所以我們需要獲得 RippleWrapper 相對(duì)整個(gè)頁面的坐標(biāo)(通過 getBoundingClientRect),然后二者相減。獲取元素位置的相關(guān)操作,可以參見用Javascript獲取頁面元素的位置 - 阮一峰的網(wǎng)絡(luò)日志。
start (e) { const { center, timeout } = this.props; const element = ReactDOM.findDOMNode(this); const rect = element ? element.getBoundingClientRect() : { left: 0, right: 0, width: 0, height: 0, }; let rippleX, rippleY, rippleSize; // 計(jì)算坐標(biāo) if ( center || (e.clientX === 0 && e.clientY === 0) || (!e.clientX && !e.touches) ) { rippleX = Math.round(rect.width / 2); rippleY = Math.round(rect.height / 2); } else { const clientX = e.clientX ? e.clientX : e.touches[0].clientX; const clientY = e.clientY ? e.clientY : e.touches[0].clientY; rippleX = Math.round(clientX - rect.left); rippleY = Math.round(clientY - rect.top); } // 計(jì)算大小 if (center) { rippleSize = Math.sqrt((2 * Math.pow(rect.width, 2) + Math.pow(rect.height, 2)) / 3); } else { const sizeX = Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2; const sizeY = Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2; rippleSize = Math.sqrt(Math.pow(sizeX, 2) + Math.pow(sizeY, 2)); } this.createRipple({ rippleX, rippleY, rippleSize, timeout }); }
關(guān)于 stop,沒啥可說的,移除 rippleArray 的第一個(gè)元素即可。
stop (e) { const { rippleArray } = this.state; if (rippleArray && rippleArray.length) { this.setState({ rippleArray: rippleArray.slice(1), }); } }createRipple
這個(gè)函數(shù)即創(chuàng)建 Ripple 使用的。start 函數(shù)最后一步使用計(jì)算出來的各項(xiàng)參數(shù)調(diào)用它。createRipple 就會(huì)構(gòu)建一個(gè) Ripple,然后將其放入 rippleArray 中。
注意這個(gè) nextKey,這是 React 要求的,數(shù)組中每個(gè)元素都要有一個(gè)不同的 key,以便在調(diào)度過程中提高效率
createRipple (params) { const { rippleX, rippleY, rippleSize, timeout } = params; let rippleArray = this.state.rippleArray; rippleArray = [ ...rippleArray,其他]; this.setState({ rippleArray: rippleArray, nextKey: this.state.nextKey + 1, }); }
RippleWrapper 這個(gè)組件的核心功能基本講完了,還有一些其他需要優(yōu)化的點(diǎn):
移動(dòng)端 touch 事件的觸發(fā)非常快,有時(shí) Ripple 還沒有創(chuàng)建出來就被 stop 了,所以需要給 touch 事件創(chuàng)建的 Ripple 一個(gè)延時(shí)。
touchstart 的同時(shí)會(huì)觸發(fā) mousedown 事件,于是在移動(dòng)端一次點(diǎn)擊會(huì)“尷尬”地創(chuàng)建兩個(gè) Ripple。這里需要設(shè)置一個(gè) flag,標(biāo)記是否需要忽略 mousedown 的觸發(fā)。
這些細(xì)節(jié)就不展開講解了,感興趣的讀者可以參見源碼。
最后總結(jié)了以上功能我實(shí)現(xiàn)了 react-touch-ripple 這個(gè)庫(kù),同時(shí)引入了單元測(cè)試,flowtype 等特性,提供了一個(gè)比較簡(jiǎn)潔的 API,有此需求的讀者可以直接使用。
附上源碼:https://github.com/froyog/react-touch-ripple
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/94944.html
摘要:個(gè)人博客凌霄的博客前言作為一個(gè)黨,我很喜歡的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 個(gè)人博客:凌霄的博客 前言 作為一個(gè) md 黨,我很喜歡 md 的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 效果 showImg(https://segmentfault.com/img/remote/1460000009254787?w=304&h=161); 開始 ...
摘要:個(gè)人博客凌霄的博客前言作為一個(gè)黨,我很喜歡的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 個(gè)人博客:凌霄的博客 前言 作為一個(gè) md 黨,我很喜歡 md 的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 效果 showImg(https://segmentfault.com/img/remote/1460000009254787?w=304&h=161); 開始 ...
摘要:個(gè)人博客凌霄的博客前言作為一個(gè)黨,我很喜歡的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 個(gè)人博客:凌霄的博客 前言 作為一個(gè) md 黨,我很喜歡 md 的漣漪效果,百度找到的源碼都太復(fù)雜,于是動(dòng)手自己寫了一個(gè)。 效果 showImg(https://segmentfault.com/img/remote/1460000009254787?w=304&h=161); 開始 ...
摘要:但是往往要引入一大堆和,其實(shí)在已有的項(xiàng)目中,可能只是想加一個(gè)這樣的按鈕,來增強(qiáng)用戶體驗(yàn),這些庫(kù)就顯得有些過于龐大了,同時(shí)由于是實(shí)現(xiàn),很多時(shí)候還要注意加載問題。 前言 大家平時(shí)應(yīng)該經(jīng)常見到這種特效,很炫酷不是嗎 showImg(https://segmentfault.com/img/remote/1460000016740061?w=318&h=190); 這是谷歌Material D...
摘要:但是往往要引入一大堆和,其實(shí)在已有的項(xiàng)目中,可能只是想加一個(gè)這樣的按鈕,來增強(qiáng)用戶體驗(yàn),這些庫(kù)就顯得有些過于龐大了,同時(shí)由于是實(shí)現(xiàn),很多時(shí)候還要注意加載問題。 前言 大家平時(shí)應(yīng)該經(jīng)常見到這種特效,很炫酷不是嗎 showImg(https://segmentfault.com/img/remote/1460000016740061?w=318&h=190); 這是谷歌Material D...
閱讀 1640·2021-09-02 09:55
閱讀 1108·2019-08-30 13:19
閱讀 1403·2019-08-26 13:51
閱讀 1452·2019-08-26 13:49
閱讀 2380·2019-08-26 12:13
閱讀 461·2019-08-26 11:52
閱讀 1908·2019-08-26 10:58
閱讀 3089·2019-08-26 10:19