最近公司需要实现一个卡片堆叠滑动效果,小编找了一圈没找到合适的决定手搓一个,先看效果:
HTML结构
HTML结构非常简单,我们创建一个div容器来包含所有的卡片,每个卡片用一个div标签来表示,并在每个卡片中放置一张图片。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>循环滑动卡片</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="card-stack">
<div class="card">
<img src="./Card1.svg" alt="Card 1">
</div>
<div class="card">
<img src="./Card2.svg" alt="Card 2">
</div>
<div class="card">
<img src="./Card4.svg" alt="Card 4">
</div>
<div class="card">
<img src="./Card5.svg" alt="Card 5">
</div>
</div>
<script src="index.js"></script>
</body>
</html>
css结构
css中主要是在.card样式中,我们设置了绝对定位、固定宽高、圆角边框、居中对齐以及过渡效果,使卡片在滑动时有平滑的动画效果。
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #248c46;
}
#card-stack {
position: relative;
width: 300px;
height: 200px;
}
.card {
position: absolute;
width: 100%;
height: 100%;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
transition: transform 0.5s ease-in-out, opacity 0.5s ease-in-out;
opacity: 1; /* 默认情况下卡片是可见的 */
}
.card img {
width: 100%;
height: 100%;
}
.hidden {
opacity: 0; /* 隐藏的卡片 */
transform: translateX(100%); /* 将隐藏卡片移出视野 */
}
JavaScript逻辑
- 在初始化时获取
cardStack容器和所有卡片的数组cards updateCards函数根据卡片在数组中的位置更新每张卡片的样式,包括zIndex、opacity和transform属性。- 最上面的卡片设置最高的
zIndex和完全不透明,其他卡片依次递减。 handleSwipe函数根据滑动方向更新当前卡片的样式,使其滑出视图,并在500毫秒后将其移到堆叠末尾,并重置样式。- 使用
setTimeout延迟执行,使滑动动画看起来更平滑 6.handleTouchEvent函数根据起始和结束位置计算滑动方向,并调用handleSwipe处理滑动逻辑。 - 仅在滑动距离超过50像素时才触发滑动行为
- 在
touchstart事件中,我们记录起始位置touchStartX并调用e.preventDefault()阻止默认行为。 - 在
touchmove和touchend事件中同样阻止默认行为,并在touchend事件中调用handleTouchEvent处理滑动逻辑。 - 在
mousedown事件中,我们记录起始位置mouseStartX并设置isMouseDown为true。 - 在
mousemove事件中,如果isMouseDown为true,我们阻止默认行为。 - 在
mouseup和mouseleave事件中,如果isMouseDown为true,我们调用handleTouchEvent处理滑动逻辑,并重置isMouseDown为false。
document.addEventListener('DOMContentLoaded', () => {
const cardStack = document.getElementById('card-stack');
let cards = Array.from(document.querySelectorAll('.card'));
let isSwiping = false;
let isMouseDown = false;
// 更新卡片层叠样式
const updateCards = () => {
cards.forEach((card, index) => {
card.classList.remove('hidden');
if (index === 0) {
card.style.zIndex = cards.length;
card.style.opacity = 1;
card.style.transform = isSwiping ? card.style.transform : 'translateY(0) scale(1)';
} else {
card.style.zIndex = cards.length - index;
card.style.opacity = 1 - index * 0.2;
card.style.transform = `translateY(${-index * 20}px) scale(${1 - index * 0.1})`;
}
});
};
// 处理滑动行为
const handleSwipe = (direction) => {
isSwiping = true;
const currentCard = cards[0];
currentCard.style.transform = `translateX(${direction === 'left' ? '-100%' : '100%'})`;
currentCard.style.opacity = 0;
setTimeout(() => {
currentCard.classList.add('hidden');
cardStack.appendChild(currentCard);
cards.push(cards.shift());
isSwiping = false;
updateCards();
currentCard.style.transform = '';
}, 500);
};
// 处理触摸事件
const handleTouchEvent = (startX, endX) => {
if (!isSwiping && Math.abs(startX - endX) > 50) {
handleSwipe(startX > endX ? 'left' : 'right');
}
};
let touchStartX = 0;
let mouseStartX = 0;
// 触摸开始事件
document.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
e.preventDefault();
});
// 触摸移动事件,阻止默认行为
document.addEventListener('touchmove', (e) => {
e.preventDefault();
});
// 触摸结束事件
document.addEventListener('touchend', (e) => {
handleTouchEvent(touchStartX, e.changedTouches[0].screenX);
e.preventDefault();
});
// 鼠标按下事件
const startSwipe = (e) => {
mouseStartX = e.screenX;
isMouseDown = true;
e.preventDefault();
};
// 鼠标松开事件
const endSwipe = (e) => {
if (isMouseDown) {
handleTouchEvent(mouseStartX, e.screenX);
isMouseDown = false;
e.preventDefault();
}
};
// 添加鼠标事件监听器
cardStack.addEventListener('mousedown', startSwipe);
cardStack.addEventListener('mousemove', (e) => {
if (isMouseDown) e.preventDefault();
});
cardStack.addEventListener('mouseup', endSwipe);
cardStack.addEventListener('mouseleave', endSwipe);
// 初始化卡片
updateCards();
});
react代码
import React, { useState, useEffect, useRef } from "react";
import "./CardStack.css";
import Image from "next/image";
interface Card {
id: number;
image: string;
}
const cardsData: Card[] = [
{ id: 1, image: crad1 },
{ id: 2, image: crad2 },
// { id: 3, image: crad3 },
{ id: 4, image: crad4 },
{ id: 5, image: crad5 },
];
const CardCarousel: React.FC = () => {
const [cards, setCards] = useState<Card[]>(cardsData);
const [isSwiping, setIsSwiping] = useState(false);
const touchStartX = useRef(0);
const touchEndX = useRef(0);
const touchStartY = useRef(0);
const touchEndY = useRef(0);
const mouseStartX = useRef(0);
const mouseEndX = useRef(0);
const isMouseDown = useRef(false);
const cardStackRef = useRef<HTMLDivElement>(null);
useEffect(() => {
updateCards();
}, [cards]);
useEffect(() => {
const cardStack = cardStackRef.current;
if (cardStack) {
cardStack.addEventListener("touchstart", handleTouchStart, {
passive: false,
});
cardStack.addEventListener("touchmove", handleTouchMove, {
passive: false,
});
cardStack.addEventListener("touchend", handleTouchEnd, {
passive: false,
});
return () => {
cardStack.removeEventListener("touchstart", handleTouchStart);
cardStack.removeEventListener("touchmove", handleTouchMove);
cardStack.removeEventListener("touchend", handleTouchEnd);
};
}
}, []);
const updateCards = () => {
const cardElements = Array.from(
document.querySelectorAll(".card")
) as HTMLElement[];
cardElements.forEach((card, index) => {
card.classList.remove("hidden");
if (index === 0) {
card.style.transform = isSwiping
? card.style.transform
: "translateY(0) scale(1)";
card.style.zIndex = `${cards.length}`;
card.style.opacity = "1";
} else {
card.style.transform = `translateY(${-index * 20}px) scale(${
1 - index * 0.1
})`;
card.style.zIndex = `${cards.length - index}`;
card.style.opacity = `${1 - index * 0.2}`;
}
});
};
const handleSwipe = (direction: "left" | "right") => {
setIsSwiping(true);
const currentCard = document.querySelector(
".card:first-child"
) as HTMLElement;
if (direction === "left") {
currentCard.style.transform = "translateX(-100%)";
currentCard.style.opacity = "0";
} else if (direction === "right") {
currentCard.style.transform = "translateX(100%)";
currentCard.style.opacity = "0";
}
setTimeout(() => {
currentCard.classList.add("hidden");
setCards((prevCards) => [...prevCards.slice(1), prevCards[0]]);
setIsSwiping(false);
currentCard.style.transform = "";
}, 500);
};
const handleTouchStart = (e: TouchEvent) => {
touchStartX.current = e.changedTouches[0].screenX;
touchStartY.current = e.changedTouches[0].screenY;
};
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault(); // Prevent default browser behavior during touch move
};
const handleTouchEnd = (e: TouchEvent) => {
touchEndX.current = e.changedTouches[0].screenX;
touchEndY.current = e.changedTouches[0].screenY;
const deltaX = touchStartX.current - touchEndX.current;
const deltaY = touchStartY.current - touchEndY.current;
// Check if the swipe is horizontal and significant
if (!isSwiping && Math.abs(deltaX) > 50 && Math.abs(deltaY) < 50) {
if (deltaX > 50) {
handleSwipe("left");
} else if (deltaX < -50) {
handleSwipe("right");
}
}
};
const startSwipe = (e: React.MouseEvent) => {
mouseStartX.current = e.screenX;
isMouseDown.current = true;
};
const endSwipe = (e: React.MouseEvent) => {
if (isMouseDown.current) {
mouseEndX.current = e.screenX;
isMouseDown.current = false;
if (
!isSwiping &&
Math.abs(mouseStartX.current - mouseEndX.current) > 50
) {
if (mouseStartX.current - mouseEndX.current > 50) {
handleSwipe("left");
} else if (mouseEndX.current - mouseStartX.current > 50) {
handleSwipe("right");
}
}
}
};
return (
<div
id="card-stack"
ref={cardStackRef}
onMouseDown={startSwipe}
onMouseUp={endSwipe}
onMouseLeave={endSwipe}
>
{cards.map((card) => (
<div key={card.id} className="card">
<Image src={card.image} alt={`Card ${card.id}`} />
</div>
))}
</div>
);
};
export default CardCarousel;