思路
其实实现思路就是设置两个变量 isEven
和isAnimating
通过isEven
来控制当前展示哪个div
;isAnimating
来管理当前状态。
核心 Api 就是 transitionend 事件监听动画结束。
状态分为两种 进行中: next
下一个进入 active
当前展示
准备中: enter
从下方进入时的动画目标状态 exit
向上退出时的动画目标状态
代码实现
javascript
import React, { useState, useEffect, useMemo } from 'react';
import styles from './index.module.scss';
const NewSwiper = ({ items, speed = 4000 }) => {
// `isEven` 用于跟踪两个轮播槽位('偶数槽'或'奇数槽')中哪个是当前激活的
const [isEven, setIsEven] = useState(true);
// `currentIndex` 指向当前在激活槽中可见的项目索引
const [currentIndex, setCurrentIndex] = useState(0);
// `isAnimating` 是控制是否应用动画类的标志
const [isAnimating, setIsAnimating] = useState(false);
const nextIndex = useMemo(() => {
if (!items || items.length === 0) return 0;
return (currentIndex + 1) % items.length;
}, [currentIndex, items]);
useEffect(() => {
// 如果只有一个项目或没有项目,则不执行动画
if (items.length <= 1) {
return;
}
const intervalId = setInterval(() => {
setIsAnimating(true);
}, speed);
// 组件卸载时清除定时器
return () => clearInterval(intervalId);
}, [items.length, speed]);
/**
* 当退出元素的 CSS 过渡动画结束时调用此函数
* 它负责重置状态以准备下一个动画循环:
* 1. 更新当前显示的索引
* 2. 切换活动槽位
* 3. 重置动画标志
*/
const handleTransitionEnd = () => {
setCurrentIndex(nextIndex);
setIsEven(prev => !prev);
setIsAnimating(false);
};
// 如果没有项目,返回空或占位组件
if (!items || items.length === 0) {
return null;
}
// 根据 isEven 状态确定每个槽位应该显示哪个项目
const evenItem = isEven ? items[currentIndex] : items[nextIndex];
const oddItem = isEven ? items[nextIndex] : items[currentIndex];
// 根据当前状态为每个槽位确定正确的 CSS 类名,控制动画效果
const evenSlotClass = isAnimating
? (isEven ? styles.slotExit : styles.slotEnter)
: (isEven ? styles.slotActive : styles.slotNext);
const oddSlotClass = isAnimating
? (isEven ? styles.slotEnter : styles.slotExit)
: (isEven ? styles.slotNext : styles.slotActive);
return (
<div className={styles.swiperContainer} title="NewSwiper Container">
<div
key="even" // 使用静态 key,因为我们要复用 DOM 元素
className={`${styles.swiperSlot} ${evenSlotClass}`}
// 只在元素退出视图时添加过渡结束事件处理程序
onTransitionEnd={isAnimating && isEven ? handleTransitionEnd : undefined}
>
{evenItem?.content}
</div>
<div
key="odd" // 使用静态 key,因为我们要复用 DOM 元素
className={`${styles.swiperSlot} ${oddSlotClass}`}
onTransitionEnd={isAnimating && !isEven ? handleTransitionEnd : undefined}
>
{oddItem?.content}
</div>
</div>
);
};
export default NewSwiper;
css
.swiperContainer {
position: relative;
height: 40px;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
border-radius: 8px;
color: #333;
}
.swiperSlot {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
}
.slotNext {
opacity: 0;
transform: translateY(100%);
}
.slotActive {
opacity: 1;
transform: translateY(0%);
}
.slotEnter {
opacity: 1;
transform: translateY(0%);
}
.slotExit {
opacity: 0;
transform: translateY(-100%);
}

