媒介
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
的第一個元素。
Ripple
Ripple 這個組件的完成比較簡樸,它是一個純函數。起首依據 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 (
<Transition
onEnter={this.handleEnter}
onExit={this.handleExit}
timeout={timeout}
{...other}
>
<span className={wrapperExiting ? 'rtr-ripple-wrapper-exiting' : ''}>
<span
className={rippleEntering ? 'rtr-ripple-entering' : ''}
style={{
width: rippleSize,
height: rippleSize,
top: rippleY - (rippleSize / 2),
left: rippleX - (rippleSize / 2),
backgroundColor: color,
}}
/>
</span>
</Transition>
);
}
}
注重這兩個 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 () {
<TransitionGroup
component="span"
enter
exit
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseLeave={this.handleMouseLeave}
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
onTouchMove={this.handleTouchMove}
>
{this.state.rippleArray}
</TransitionGroup>
}
}
這裏的 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
和 stop
start 這個函數擔任盤算事宜發作的坐標,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,
<Ripple
timeout={timeout}
color={this.props.color}
key={this.state.nextKey}
rippleX={rippleX}
rippleY={rippleY}
rippleSize={rippleSize}
/>
];
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,有此需求的讀者能夠直接運用。