基於 React 實現一個 Transition 過渡動畫元件
過渡動畫使 UI 更富有表現力並且易於使用。如何使用 React 快速的實現一個 Transition 過渡動畫元件?
基本實現
實現一個基礎的 CSS 過渡動畫元件,通過切換 CSS 樣式實現簡單的動畫效果,也就是通過新增或移除某個 class 樣式。因此需要給 Transition 元件新增一個 toggleClass 屬性,標識要切換的 class 樣式,再新增一個 action 屬性實現樣式切換,action 為 true 時新增 toggleClass 到動畫元素上,action 為 false 時移除 toggleClass。
安裝 classnames 外掛:
npm install classnames --save-dev
classnames 是一個簡單的JavaScript實用程式,用於有條件地將 className 連線在一起。
在 components 目錄下新建一個 Transition 資料夾,並在該資料夾下新建一個 Transition.jsx 檔案:
import React from 'react' import classnames from 'classnames' /** * css過渡動畫元件 * * @visibleName Transition 過渡動畫 */ class Transition extends React.Component { render() { const { children } = this.props const transition = ( <div className={ classnames({ transition: true }) } style={ { position: 'relative', overflow: 'hidden' } } > <div className={ classnames({ 'transition-wrapper': true }) } > { children } </div> </div> ) return transition } } export default Transition
這裡使用了 JSX,在 JSX 中,使用 camelCase(小駝峰命名)來定義屬性的名稱,使用大括號“{}”嵌入任何有效的 JavaScript 表示式。
如:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
等價於:
const element = <h1>Hello, Josh Perez</h1>;
注意:
因為 JSX 語法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小駝峰命名)來定義屬性的名稱,而不使用 HTML 屬性名稱的命名約定。
例如,JSX 裡的 class 變成了 className,而 tabindex 則變為 tabIndex。
另外,在 React 中,props.children
包含元件所有的子節點,即元件的開始標籤和結束標籤之間的內容(與 Vue 中 slot 插槽相似)。例如:
<Button>預設按鈕</Button>
在 Button 元件中獲取 props.children,就可以得到字串“預設按鈕”。
接下來,在 Transition 資料夾下新建一個 index.js,匯出 Transition 元件:
import Transition from './Transition.jsx'
export { Transition }
export default Transition
然後,在 Transition.jsx 檔案中為元件新增 props 檢查並設定 action 的預設值:
import PropTypes from 'prop-types'
const propTypes = {
/** 執行動畫 */
action: PropTypes.bool,
/** 切換的css動畫的class名稱 */
toggleClass: PropTypes.string
}
const defaultProps = {
action: false
}
這裡使用了 prop-types 實現執行時型別檢查。
注意:
prop-types 是一個執行時型別檢查工具,也是 create-react-app 腳手架預設配置的執行時型別檢查工具,使用時直接引入即可,無需安裝。
完整的 Transition 元件程式碼如下:
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const propTypes = {
/** 執行動畫 */
action: PropTypes.bool,
/** 切換的css動畫的class名稱 */
toggleClass: PropTypes.string
}
const defaultProps = {
action: false
}
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
static propTypes = propTypes
static defaultProps = defaultProps
render() {
const {
className,
action,
toggleClass,
children
} = this.props
const transition = (
<div
className={
classnames({
transition: true
})
}
style={
{
position: 'relative',
overflow: 'hidden'
}
}
>
<div
className={
classnames({
'transition-wrapper': true,
[className]: className,
[toggleClass]: action && toggleClass
})
}
>
{ children }
</div>
</div>
)
return transition
}
}
export default Transition
現在,可以使用我們的 Transition 元件了。
CSS 程式碼如下:
.fade {
transition: opacity 0.15s linear;
}
.fade:not(.show) {
opacity: 0;
}
JS 程式碼如下:
import React from 'react';
import Transition from './Transition';
class Anime extends React.Component {
constructor (props) {
super(props)
this.state = {
action: true
}
}
render () {
const btnText = this.state.action ? '淡出' : '淡入'
return (
<div>
<Transition
className="fade"
toggleClass="show"
action={ this.state.action }
>
淡入淡出
</Transition>
<button
style={{ marginTop: '20px' }}
onClick={() => this.setState({ action: !this.state.action })}
>
{ btnText }
</button>
</div>
)
}
}
然後,在你需要該動畫的地方使用 Anime 元件即可。
實現 Animate.css 相容
Animate.css 是一款強大的預設 CSS3 動畫庫。接下來,實現在 Transition 元件中使用 Animate.css 實現強大的 CSS3 動畫。
由於 Animate.css 動畫在進入動畫和離開動畫通常使用兩個效果相反的 class 樣式,因此,需要給 Transition 元件新增 enterClass 和 leaveClass 兩個屬性,實現動畫切換。
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const propTypes = {
/** 執行動畫 */
action: PropTypes.bool,
/** 切換的css動畫的class名稱 */
toggleClass: PropTypes.string,
/** 進入動畫的class名稱,存在 toggleClass 時無效 */
enterClass: PropTypes.string,
/** 離開動畫的class名稱,存在 toggleClass 時無效 */
leaveClass: PropTypes.string
}
const defaultProps = {
action: false
}
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
static propTypes = propTypes
static defaultProps = defaultProps
render() {
const {
className,
action,
toggleClass,
enterClass,
leaveClass,
children
} = this.props
return (
<div
className={
classnames({
transition: true
})
}
style={
{
position: 'relative',
overflow: 'hidden'
}
}
>
<div
className={
classnames({
'transition-wrapper': true,
[className]: className,
[toggleClass]: action && toggleClass,
[enterClass]: !toggleClass && action && enterClass,
[leaveClass]: !toggleClass && !action && leaveClass,
})
}
>
{ children }
</div>
</div>
)
}
}
export default Transition
注意:
由於 toggleClass 適用於那些進入動畫與離開動畫切換相同 class 樣式的情況,而 enterClass 和 leaveClass 適用於那些進入動畫與離開動畫切換不同的 class 樣式的情況,所以,他們與 toggleClass 不能共存。
接下來,就可以試一試加入 Animate.css 後的 Transition 元件:
import React from 'react';
import 'animate.css';
class Anime extends React.Component {
constructor (props) {
super(props)
this.state = {
action: true
}
}
render () {
return (
<div>
<Transition
className="animated"
enterClass="bounceInLeft"
leaveClass="bounceOutLeft"
action={ this.state.action }
>
彈入彈出
</Transition>
<utton
style={{ marginTop: '20px' }}
onClick={() => this.setState({ action: !this.state.action })}
>
{ this.state.action ? '彈出' : '彈入' }
</utton>
</div>
)
}
}
功能擴充套件
通過上面的實現,Transition 元件能適用大部分場景,但是功能不夠豐富。因此,接下來就需要擴充套件 Transition 的介面。動畫通常可以設定延遲時間,播放時長,播放次數等屬性。因此,需要給 Transition 新增這些屬性,來豐富設定動畫。
新增如下 props 屬性,並設定預設值:
const propTypes = {
...,
/** 動畫延遲執行時間 */
delay: PropTypes.string,
/** 動畫執行時間長度 */
duration: PropTypes.string,
/** 動畫執行次數,只在執行 CSS3 動畫時有效 */
count: PropTypes.number,
/** 動畫緩動函式 */
easing: PropTypes.oneOf([
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out'
]),
/** 是否強制輪流反向播放動畫,count 為 1 時無效 */
reverse: PropTypes.bool
}
const defaultProps = {
count: 1,
reverse: false
}
根據 props 設定樣式:
// 動畫樣式
const styleText = (() => {
let style = {}
// 設定延遲時長
if (delay) {
style.transitionDelay = delay
style.animationDelay = delay
}
// 設定播放時長
if (duration) {
style.transitionDuration = duration
style.animationDuration = duration
}
// 設定播放次數
if (count) {
style.animationIterationCount = count
}
// 設定緩動函式
if (easing) {
style.transitionTimingFunction = easing
style.animationTimingFunction = easing
}
// 設定動畫方向
if (reverse) {
style.animationDirection = 'alternate'
}
return style
})()
完整程式碼如下:
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const propTypes = {
/** 執行動畫 */
action: PropTypes.bool,
/** 切換的css動畫的class名稱 */
toggleClass: PropTypes.string,
/** 進入動畫的class名稱,存在 toggleClass 時無效 */
enterClass: PropTypes.string,
/** 離開動畫的class名稱,存在 toggleClass 時無效 */
leaveClass: PropTypes.string,
/** 動畫延遲執行時間 */
delay: PropTypes.string,
/** 動畫執行時間長度 */
duration: PropTypes.string,
/** 動畫執行次數,只在執行 CSS3 動畫時有效 */
count: PropTypes.number,
/** 動畫緩動函式 */
easing: PropTypes.oneOf([
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out'
]),
/** 是否強制輪流反向播放動畫,count 為 1 時無效 */
reverse: PropTypes.bool
}
const defaultProps = {
action: false,
count: 1,
reverse: false
}
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
static propTypes = propTypes
static defaultProps = defaultProps
render() {
const {
className,
action,
toggleClass,
enterClass,
leaveClass,
delay,
duration,
count,
easing,
reverse,
children
} = this.props
// 動畫樣式
const styleText = (() => {
let style = {}
// 設定延遲時長
if (delay) {
style.transitionDelay = delay
style.animationDelay = delay
}
// 設定播放時長
if (duration) {
style.transitionDuration = duration
style.animationDuration = duration
}
// 設定播放次數
if (count) {
style.animationIterationCount = count
}
// 設定緩動函式
if (easing) {
style.transitionTimingFunction = easing
style.animationTimingFunction = easing
}
// 設定動畫方向
if (reverse) {
style.animationDirection = 'alternate'
}
return style
})()
return (
<div
className={
classnames({
transition: true
})
}
style={
{
position: 'relative',
overflow: 'hidden'
}
}
>
<div
className={
classnames({
'transition-wrapper': true,
[className]: className,
[toggleClass]: action && toggleClass,
[enterClass]: !toggleClass && action && enterClass,
[leaveClass]: !toggleClass && !action && leaveClass,
})
}
style={ styleText }
>
{ children }
</div>
</div>
)
}
}
export default Transition
這裡為 Transition 增加了以下設定屬性:
- delay:規定在動畫開始之前的延遲。
- duration:規定完成動畫所花費的時間,以秒或毫秒計。
- count:規定動畫應該播放的次數。
- easing:規定動畫的速度曲線。
- reverse:規定是否應該輪流反向播放動畫。
目前,Transition 的功能已經相當豐富,可以很精細的控制 CSS3 動畫。
優化
這一步,我們需要針對 Transition 元件進一步優化,主要包括動畫結束的監聽、解除安裝元件以及相容。
新增以下 props 屬性,並設定預設值:
const propTypes = {
...,
/** 動畫結束的回撥 */
onEnd: PropTypes.func,
/** 離開動畫結束時解除安裝元素 */
exist: PropTypes.bool
}
const defaultProps = {
...,
reverse: false,
exist: false
}
處理動畫結束的監聽事件:
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
...
onEnd = e => {
const { onEnd, action, exist } = this.props
if (onEnd) {
onEnd(e)
}
// 解除安裝 DOM 元素
if (!action && exist) {
const node = e.target.parentNode
node.parentNode.removeChild(node)
}
}
/**
* 對動畫結束事件 onEnd 回撥的處理函式
*
* @param {string} type - 事件解繫結型別: add - 繫結事件,remove - 移除事件繫結
*/
handleEndListener (type = 'add') {
const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
const events = ['animationend', 'transitionend']
events.forEach(ev => {
el[`${type}EventListener`](ev, this.onEnd, false)
})
}
componentDidMount () {
this.handleEndListener()
}
componentWillUnmount () {
const { action, exist } = this.props
if (!action && exist) {
this.handleEndListener('remove')
}
}
render () {
...
}
}
這裡使用到兩個生命週期函式 componentDidMount 和 componentWillUnmount,關於 React 生命週期的介紹請移步元件生命週期。
react-dom 提供了可在 React 應用中使用的 DOM 方法。
獲取相容性的 animationend 事件和 transitionend 事件。不同的瀏覽器要求使用不同的字首,因為火狐和IE都已經支援了這兩個事件,因此,只需針對 webkit 核心瀏覽器進行相容的 webkitTransitionEnd 事件檢測。檢測函式程式碼如下:
/**
* 瀏覽器相容事件檢測函式
*
* @param {node} el - 觸發事件的 DOM 元素
* @param {array} events - 可能的事件型別
* @returns {*}
*/
const whichEvent = (el, events) => {
const len = events.length
for (var i = 0; i < len; i++) {
if (el.style[i]) {
return events[i];
}
}
}
修改 handleEndListener 函式:
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
...
/**
* 對動畫結束事件 onEnd 回撥的處理函式
*
* @param {string} type - 事件解繫結型別: add - 繫結事件,remove - 移除事件繫結
*/
handleEndListener (type = 'add') {
const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
const events = ['AnimationEnd', 'TransitionEnd']
events.forEach(ev => {
const eventType = whichEvent(el, [ev.toLowerCase(), `webkit${ev}`])
el[`${type}EventListener`](eventType, this.onEnd, false)
})
}
...
}
到這裡,我們完成了整個 Transition 元件的開發,完整程式碼如下:
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import ReactDOM from 'react-dom'
const propTypes = {
/** 執行動畫 */
action: PropTypes.bool,
/** 切換的css動畫的class名稱 */
toggleClass: PropTypes.string,
/** 進入動畫的class名稱,存在 toggleClass 時無效 */
enterClass: PropTypes.string,
/** 離開動畫的class名稱,存在 toggleClass 時無效 */
leaveClass: PropTypes.string,
/** 動畫延遲執行時間 */
delay: PropTypes.string,
/** 動畫執行時間長度 */
duration: PropTypes.string,
/** 動畫執行次數,只在執行 CSS3 動畫時有效 */
count: PropTypes.number,
/** 動畫緩動函式 */
easing: PropTypes.oneOf([
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out'
]),
/** 是否強制輪流反向播放動畫,count 為 1 時無效 */
reverse: PropTypes.bool,
/** 動畫結束的回撥 */
onEnd: PropTypes.func,
/** 離開動畫結束時解除安裝元素 */
exist: PropTypes.bool
}
const defaultProps = {
action: false,
count: 1,
reverse: false,
exist: false
}
/**
* 瀏覽器相容事件檢測函式
*
* @param {node} el - 觸發事件的 DOM 元素
* @param {array} events - 可能的事件型別
* @returns {*}
*/
const whichEvent = (el, events) => {
const len = events.length
for (var i = 0; i < len; i++) {
if (el.style[i]) {
return events[i];
}
}
}
/**
* css過渡動畫元件
*
* @visibleName Transition 過渡動畫
*/
class Transition extends React.Component {
static propTypes = propTypes
static defaultProps = defaultProps
onEnd = e => {
const { onEnd, action, exist } = this.props
if (onEnd) {
onEnd(e)
}
// 解除安裝 DOM 元素
if (!action && exist) {
const node = e.target.parentNode
node.parentNode.removeChild(node)
}
}
/**
* 對動畫結束事件 onEnd 回撥的處理函式
*
* @param {string} type - 事件解繫結型別: add - 繫結事件,remove - 移除事件繫結
*/
handleEndListener (type = 'add') {
const el = ReactDOM.findDOMNode(this).querySelector('.transition-wrapper')
const events = ['AnimationEnd', 'TransitionEnd']
events.forEach(ev => {
const eventType = whichEvent(el, [ev.toLowerCase(), `webkit${ev}`])
el[`${type}EventListener`](eventType, this.onEnd, false)
})
}
componentDidMount () {
this.handleEndListener()
}
componentWillUnmount() {
const { action, exist } = this.props
if (!action && exist) {
this.handleEndListener('remove')
}
}
render () {
const {
className,
action,
toggleClass,
enterClass,
leaveClass,
delay,
duration,
count,
easing,
reverse,
children
} = this.props
// 動畫樣式
const styleText = (() => {
let style = {}
// 設定延遲時長
if (delay) {
style.transitionDelay = delay
style.animationDelay = delay
}
// 設定播放時長
if (duration) {
style.transitionDuration = duration
style.animationDuration = duration
}
// 設定播放次數
if (count) {
style.animationIterationCount = count
}
// 設定緩動函式
if (easing) {
style.transitionTimingFunction = easing
style.animationTimingFunction = easing
}
// 設定動畫方向
if (reverse) {
style.animationDirection = 'alternate'
}
return style
})()
const transition = (
<div
className={
classnames({
transition: true
})
}
style={
{
position: 'relative',
overflow: 'hidden'
}
}
>
<div
className={
classnames({
'transition-wrapper': true,
[className]: className,
[toggleClass]: action && toggleClass,
[enterClass]: !toggleClass && action && enterClass,
[leaveClass]: !toggleClass && !action && leaveClass,
})
}
style={ styleText }
>
{ children }
</div>
</div>
)
return transition
}
}
export default Transition
原文地址:基於 React 實現一個 Transition 過渡動畫組