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