【教程】React 完成 Material Design 中蕩漾(Ripple)結果

媒介

Material Design 推出已有靠近4年,人人對“觸摸蕩漾”(Ripple)應當不生疏,簡樸來講就是一個水波紋結果(見下圖)。前段時間打仗了 material-ui 這個庫,看了下 Ripple 的源碼,以為並非一個異常好的完成,所以決議自身寫一個 React 組件—— React Touch Ripple。現已開源到 Github,以及相應的 Demo

《【教程】React 完成 Material Design 中蕩漾(Ripple)結果》

組件拆分

我們把組件拆分為兩個組件:RippleWrapperRipple

Ripple 就是一個圓形,蕩漾自身,它會接收 rippleXrippleY 如許的坐標在相應位置襯着,以及 rippleSize 決議其大小。

RippleWrapper 是一切 Ripple 的容器,它內部會保護一個 state: { rippleArray: [] }

一切的事宜監聽器也會綁定在 RippleWrapper 上,每次新增一個 Ripple 就將其 push 進 rippleArray 中,相應地一個 Ripple 消逝機就移除 rippleArray 的第一個元素。

《【教程】React 完成 Material Design 中蕩漾(Ripple)結果》

Ripple

Ripple 這個組件的完成比較簡樸,它是一個純函數。起首依據 Material Design 的範例,簡述下動畫襯着歷程:

  • enter 階段:ripple 逐步擴展(transform: scale(0)transform: scale(1)),同時透明度逐步增添(opacity: 0opacity: 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-enteringrtr-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; }
}

rippleXrippleYrippleSize 這些 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 分為兩部份。關於 mousedowntouchstart 這兩個事宜,就意味着須要建立一個新的 Ripple,當 mouseupmouseleavetouchendtouchmove 這些事宜觸發時,就意味着這個 Ripple 該被移除了。

注重這裡有一個“巨坑”,那就是疾速點擊時,onclick 事宜並不會被觸發。(見下圖,只輸出了 "mousedown",而沒有 "onclick"

《【教程】React 完成 Material Design 中蕩漾(Ripple)結果》

我們曉得,Ripple 的重要用途在於 button 組件,雖然我們並不處置懲罰 click 事宜,但運用者綁定的 onClick 事宜依賴於它的冒泡,假如這裏不觸發 click 的話用戶就沒法處置懲罰 button 上的點擊事宜了。這個 bug 的發作緣由直到我翻到 w3 working draft 才搞清楚。

《【教程】React 完成 Material Design 中蕩漾(Ripple)結果》

注重這句話

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 組件上,問題解決。

startstop

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,有此需求的讀者能夠直接運用。

附上源碼:https://github.com/froyog/react-touch-ripple

    原文作者:scarlet
    原文地址: https://segmentfault.com/a/1190000014835943
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞