在React中寫一個Animation元件,為元件進入和離開加上動畫/過度

tornoda發表於2019-06-23

問題

在單頁面應用中,我們經常需要給路由的切換或者元素的掛載和解除安裝加上過渡效果,為這麼一個小功能引入第三方框架,實在有點小糾結。不如自己封裝。

思路

原理

以進入時opacity: 0 --> opacity: 1 ,退出時opacity: 0 --> opacity: 1為例

元素掛載時

  1. 掛載元素dom
  2. 設定動畫opacity: 0 --> opacity: 1

元素解除安裝時

  1. 設定動畫opacity: 0 --> opacity: 1
  2. 動畫結束後解除安裝dom

元件設計

為了使得元件簡單易用、低耦合,我們期望如下方式來呼叫元件:

屬性名 型別 描述
isShow Boolean 子元素顯示或隱藏控制
name String 指定一個name,動畫進入退出時的動畫

App.jsx裡呼叫元件:

通過改變isShow的值來指定是否顯示

// App.jsx
// 其他程式碼省略
import './app.css';

<Animation isShow={isShow} name='demo'>
    <div class='demo'>
        demo
    </div>
</Animation>
// 通過改變isShow的值來指定是否顯示

App.css裡指定進入離開效果:

// 基礎樣式
.demo {
    width: 200px;
    height: 200px;
    background-color: red;
}

// 定義進出入動畫
.demo-showing {
    animation: show 0.5s forwards;
}
.demo-fading {
    animation: fade 0.5s forwards;
}

// 定義動畫fade與show
@keyframes show {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

@keyframes fade {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}

根據思路寫程式碼

// Animation.jsx
import { PureComponent } from 'react';
import './index.css';

class Animation extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            isInnerShow: false,
            animationClass: '',
        };
    }

    componentWillReceiveProps(props) {
        const { isShow } = props;
        if (isShow) {
            // 顯示
            this.show().then(() => {
                this.doShowAnimation();
            });
        } else {
            // 隱藏
            this.doFadeAnimation();
        }
    }

    handleAnimationEnd() {
        const isFading = this.state.animationClass === this.className('fading');
        if (isFading) {
            this.hide();
        }
    }

    show() {
        return new Promise(resolve => {
            this.setState(
                {
                    isInnerShow: true,
                },
                () => {
                    resolve();
                }
            );
        });
    }

    hide() {
        this.setState({
            isInnerShow: false,
        });
    }

    doShowAnimation() {
        this.setState({
            animationClass: this.className('showing'),
        });
    }

    doFadeAnimation() {
        this.setState({
            animationClass: this.className('fading'),
        });
    }

    /**
     * 獲取className
     * @param {string} inner 'showing' | 'fading'
     */
    className(inner) {
        const { name } = this.props;
        if (!name) throw new Error('animation name must be assigned');
        return `${name}-${inner}`;
    }

    render() {
        let { children } = this.props;
        children = React.Children.only(children);
        const { isInnerShow, animationClass } = this.state;
        const element = {
            ...children,
            props: {
                ...children.props,
                className: `${children.props.className} ${animationClass}`,
                onAnimationEnd: this.handleAnimationEnd.bind(this),
            },
        };
        return isInnerShow && element;
    }
}

export default Animation;

Demo示例

點我直達

相關文章