過渡動畫使 UI 更富有表現力並且易於使用。如何使用 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 過渡動畫組件