引言

在React应用中,实现过渡动画是一个常见的需求。本文将介绍如何使用react-transition-group库来实现一些常用的可控的淡入淡出过渡效果。为了便于复用,我将其封装为了一个核心组件FadeContentWrapper和两个主要的应用组件SimpleFadeTransitionSwitchFadeTransition

安装依赖

首先,确保你已经安装了react-transition-group库。

npm install react-transition-group

核心组件

FadeContentWrapper

这是一个styled-components组件,用于应用过渡效果。

import {styled} from "twin.macro";

/**
 * A styled div component that applies a fade transition effect.
 *
 * @component
 * @param {Object} props
 * @param {string} [props.fadeStyle='opacity'] - The type of transition style ('opacity', 'down', 'up').
 * @param {string} [props.duration='0.3s'] - The duration of the transition.
 * @param {string} [props.timingFunction='ease'] - The timing function for the transition.
 * @param {string} [props.offset='15px'] - The offset value, used for 'down' and 'up' styles.
 * @param {string} [props.className='fade'] - The className for the transition styles.
 *
 * @example
 * <FadeContentWrapper fadeStyle="down" duration="0.5s" timingFunction="linear" offset="20px" className="myTransition">
 *   Content goes here...
 * </FadeContentWrapper>
 */
export const FadeContentWrapper = styled.div`
  ${({fadeStyle = 'opacity', duration, timingFunction, offset, className = 'fade'}) =>
          createTransitionStyles(fadeStyle, duration, timingFunction, offset, className)
  };
`;

/**
 * Creates a CSS transition style string based on the provided parameters.
 *
 * @function
 * @param {'opacity' | 'down' | 'up' | 'left' | 'right' | 'slideFromBottom' | 'scale'} style - The type of transition style ('opacity', 'down', 'up', 'left', 'right', 'slideFromBottom', 'scale').
 * @param {string} [duration='0.3s'] - The duration of the transition.
 * @param {string} [timingFunction='ease'] - The timing function for the transition (e.g., 'linear', 'ease-in').
 * @param {string} [offset='15px'] - The offset value, used for 'down' and 'up' styles.
 * @param {string} [className='fade'] - The className for the transition styles.
 * @returns {string} The generated CSS transition style string.
 *
 * @example
 * const fadeStyle = createTransitionStyles('opacity', '0.5s', 'linear', '15px', 'myTransition');
 * const downStyle = createTransitionStyles('down', '0.4s', 'ease-in-out', '20px', 'myTransition');
 */
export function createTransitionStyles(style, duration = '0.3s', timingFunction = 'ease', offset = '15px', className = 'fade') {
    switch (style) {
        case 'opacity':
            return `
            &.${className}.enter {
                opacity: 0;
            }

            &.${className}.enter-active {
                opacity: 1;
                transition: opacity ${duration} ${timingFunction};
            }

            &.${className}.exit {
                opacity: 1;
            }

            &.${className}.exit-active {
                opacity: 0;
                transition: opacity ${duration} ${timingFunction};
            }`;

        case 'down':
            return `
            &.${className}.enter {
                transform: translateY(-${offset});
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: translateY(0);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: translateY(0);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: translateY(${offset});
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        case 'up':
            return `
            &.${className}.enter {
                transform: translateY(${offset});
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: translateY(0);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: translateY(0);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: translateY(-${offset});
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        case 'scale':
            return `
            &.${className}.enter {
                transform: scale(0);
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: scale(1);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: scale(1);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: scale(0);
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        case 'slideFromBottom':
            return `
            &.${className}.enter {
                transform: translateY(${offset});
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: translateY(0);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: translateY(0);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: translateY(${offset});
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        case 'right':
            return `
            &.${className}.enter {
                transform: translateX(-${offset});
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: translateX(0);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: translateX(0);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: translateX(${offset});
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        case 'left':
            return `
            &.${className}.enter {
                transform: translateX(${offset});
                opacity: 0;
            }

            &.${className}.enter-active {
                transform: translateX(0);
                opacity: 1;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }

            &.${className}.exit {
                transform: translateX(0);
                opacity: 1;
            }

            &.${className}.exit-active {
                transform: translateX(-${offset});
                opacity: 0;
                transition: opacity ${duration} ${timingFunction}, transform ${duration} ${timingFunction};
            }`;

        default:
            return '';
    }
}

SimpleFadeTransition

这是一个简单的淡入淡出组件。主要用于单个内容对象的可控淡入淡出动画。

import React, { useRef } from 'react';
import { CSSTransition } from 'react-transition-group';
import { FadeContentWrapper } from './FadeStyles';
/**
 * A component that provides a simple fade transition effect for a single content element.
 *
 * @param {Object} props
 * @param {boolean} props.in - Determines if the content should be visible.
 * @param {React.ReactNode} [props.children=null] - The content to display and animate.
 * @param {'opacity' | 'down' | 'up' | 'left' | 'right' | 'slideFromBottom' | 'scale'} [props.fadeStyle='opacity'] - The type of transition style.
 * @param {string} [props.className='fade'] - The className for CSSTransition.
 * @param {string} [props.duration='0.3s'] - The duration of the transition.
 * @param {string} [props.offset='15px'] - The offset value, used for 'down' and 'up' styles.
 * @param {boolean} [props.exit=true] - Determines if the exit transition should be animated.
 *
 * @example
 * <SimpleFadeTransition
 *      in={isVisible}
 *      fadeStyle={'scale'}
 *      duration={'0.2s'}
 *      className={'content-fade'}
 *      offset={'20px'}
 *      exit={false}
 * >
 *      <div>My Content</div>
 * </SimpleFadeTransition>
 *
 * @attention
 *  * 不能在条件渲染语句中使用该组件,而应该由组件自身的`in`属性来决定是否显示内容,否则会丢失过渡效果。
 */
const SimpleFadeTransition = ({
                                  in: inProp,
                                  children = null,
                                  fadeStyle = 'opacity',
                                  className = 'fade',
                                  duration = '0.3s',
                                  offset = '15px',
                                  exit = true
                              }) => {
    const nodeRef = useRef(null);

    return (
        <CSSTransition
            in={inProp}
            nodeRef={nodeRef}
            className={className}
            timeout={parseFloat(duration) * 1000} // Convert duration to milliseconds
            exit={exit}
            unmountOnExit
        >
            <FadeContentWrapper ref={nodeRef} fadeStyle={fadeStyle} className={className} duration={duration} offset={offset}>
                {children}
            </FadeContentWrapper>
        </CSSTransition>
    );
}

export default SimpleFadeTransition;

SwitchFadeTransition

这是一个用于在两个内容之间切换的组件,可以在切换的过程中应用指定过渡效果。

import React, {useRef} from 'react';
import {SwitchTransition, CSSTransition} from 'react-transition-group';
import {FadeContentWrapper} from './FadeStyles';

/**
 * A component that provides a fade transition effect between two content elements.
 *
 * @param {Object} props
 * @param {boolean} props.isOn - Determines which content to display.
 * @param {React.ReactNode} [props.onContent=null] - Content to display when isOn is true.
 * @param {React.ReactNode} [props.offContent=null] - Content to display when isOn is false.
 * @param {'opacity' | 'down' | 'up' | 'left' | 'right' | 'slideFromBottom' | 'scale'} [props.fadeStyle='opacity'] - The type of transition style ('opacity', 'down', 'up', 'left', 'right', 'slideFromBottom', 'scale').
 * @param {'out-in' | 'in-out'} [props.mode='out-in'] - The transition mode for SwitchTransition ('out-in' or 'in-out').
 * @param {string} [props.className='fade'] - The className for CSSTransition.
 * @param {string} [props.duration='0.3s'] - The duration of the transition.
 *
 * @example
 * <SwitchFadeTransition
 *      isOn={isOn}
 *      fadeStyle={'scale'}
 *      duration={'0.2s'}
 *      className={'moon-sun'}
 *      onContent={<FontAwesomeIcon icon={solid("sun")} spin tw={'text-amber-400'}/>}
 *      offContent={<FontAwesomeIcon icon={solid("moon")}/>}
 * />
 */
const SwitchFadeTransition = ({
                                  isOn,
                                  onContent = null,
                                  offContent = null,
                                  fadeStyle = 'opacity',
                                  mode = 'out-in',
                                  className = 'fade',
                                  duration = '0.3s'
                              }) => {
    const nodeRef = useRef(null);

    return (
        <SwitchTransition mode={mode}>
            <CSSTransition
                key={isOn ? "on" : "off"}
                nodeRef={nodeRef}
                className={className}
                addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
            >
                <FadeContentWrapper ref={nodeRef} fadeStyle={fadeStyle} className={className} duration={duration}>
                    {isOn ? onContent : offContent}
                </FadeContentWrapper>
            </CSSTransition>
        </SwitchTransition>
    );
}

export default SwitchFadeTransition;

使用示例

SimpleFadeTransition

<SimpleFadeTransition
  in={isVisible}
  fadeStyle={'scale'}
  duration={'0.2s'}
  className={'content-fade'}
  offset={'20px'}
  exit={false}
>
  <div>My Content</div>
</SimpleFadeTransition>

SwitchFadeTransition

<SwitchFadeTransition
  isOn={isOn}
  fadeStyle={'scale'}
  duration={'0.2s'}
  className={'moon-sun'}
  onContent={<FontAwesomeIcon icon={solid("sun")} spin tw={'text-amber-400'}/>}
  offContent={<FontAwesomeIcon icon={solid("moon")}/>}
/>

再封装一个简单的含切换动效的按钮

import React, {useEffect, useRef, useState} from 'react';
import tw from 'twin.macro';
import {UniButton} from "@/components/Button/Styled.twin";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {solid} from "@fortawesome/fontawesome-svg-core/import.macro";
import SwitchFadeTransition from "@/styles/transition/SwitchFadeTransition";

const SwitchButton = ({defaultOn = false, onChange, hasShadow = true, _tw}) => {
    const [isOn, setIsOn] = useState(defaultOn);
    const onRef = useRef(null);
    const offRef = useRef(null);
    const nodeRef = isOn ? onRef : offRef;

    const handleToggle = () => {
        const newState = !isOn;
        setIsOn(newState);
        if (onChange) {
            onChange(newState);
        }
    };

    return (
        <div tw={''}>
            <UniButton
                _tw={_tw}
                tw={'h-10 w-10 leading-10 text-blue-400 md:hover:text-blue-300'}
                onClick={handleToggle}
                hasShadow={hasShadow}
            >
                <SwitchFadeTransition
                    isOn={isOn}
                    fadeStyle={'scale'}
                    duration={'0.2s'}
                    className={'moon-sun'}
                    onContent={<FontAwesomeIcon icon={solid("sun")} spin tw={'text-amber-400'}/>}
                    offContent={<FontAwesomeIcon icon={solid("moon")}/>}
                />
            </UniButton>
        </div>
    );
};

export default SwitchButton;

效果展示

> 下载演示视频 <

注意事项

  • 不能在条件渲染语句中使用SimpleFadeTransition组件,而应该由组件自身的in属性来决定是否显示内容,否则会丢失过渡效果。

总结

通过react-transition-group库和一些基础的React知识,我们可以轻松地在React应用中实现各种复杂的过渡效果。希望这篇文章能帮助你更好地理解和使用这个强大的库。


A Student on the way to full stack of Web3.