简单的js卡片堆叠效果

426 阅读3分钟

最近公司需要实现一个卡片堆叠滑动效果,小编找了一圈没找到合适的决定手搓一个,先看效果:

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逻辑

  1. 在初始化时获取cardStack容器和所有卡片的数组cards
  2. updateCards函数根据卡片在数组中的位置更新每张卡片的样式,包括zIndexopacitytransform属性。
  3. 最上面的卡片设置最高的zIndex和完全不透明,其他卡片依次递减。
  4. handleSwipe函数根据滑动方向更新当前卡片的样式,使其滑出视图,并在500毫秒后将其移到堆叠末尾,并重置样式。
  5. 使用setTimeout延迟执行,使滑动动画看起来更平滑 6.handleTouchEvent函数根据起始和结束位置计算滑动方向,并调用handleSwipe处理滑动逻辑。
  6. 仅在滑动距离超过50像素时才触发滑动行为
  7. touchstart事件中,我们记录起始位置touchStartX并调用e.preventDefault()阻止默认行为。
  8. touchmovetouchend事件中同样阻止默认行为,并在touchend事件中调用handleTouchEvent处理滑动逻辑。
  9. mousedown事件中,我们记录起始位置mouseStartX并设置isMouseDown为true。
  10. mousemove事件中,如果isMouseDown为true,我们阻止默认行为。
  11. mouseupmouseleave事件中,如果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;