区块顶部底部吸附,支持内部元素滚动

87 阅读2分钟

Screenity video - May 26, 2025.gif

import React, { useEffect, useState, useRef, useCallback } from "react";
import "./scrollarea.css";

const ScrollArea = () => {
  const [pin, setPin] = useState("bottom"); // bottom, top
  const [top, setTop] = useState(64);
  const wrapperRef = useRef(null);
  const originalStartY = useRef(null);
  const startY = useRef(null);
  const translateY = useRef(null);
  const moving = useRef(false);
  const reachedTop = useRef(false);
  const [boudingRect, setBoudingRect] = useState([]);

  useEffect(() => {
    translateY.current = originalStartY.current = getTranslateY();
    console.log("translateY:", translateY.current, pin);
  }, [pin]);

  useEffect(() => {
    setBoudingRect([20, window.innerHeight - top]); // 设置页面中可滑动的范围
  }, []);

  const handleStart = (event) => {
    startY.current = event.touches[0].clientY;
    moving.current = true;
    console.log("handleStart:-------------", startY.current, moving.current);
  };

  const handleMove = useCallback(
    (event) => {
      if (!moving.current) return;
      const currentY = event.touches[0].clientY;
      console.log("handleMove:", currentY, startY.current, moving.current);
      const deltaY = currentY - startY.current;
      const distY = Math.max(
        boudingRect[0],
        Math.min(translateY.current + deltaY, boudingRect[1])
      );

      if (pin === "top") {
        const scrollTop = wrapperRef.current.querySelector(
          ".scroll-area-content"
        ).scrollTop;
        console.log("scrollTop:", scrollTop, deltaY, reachedTop.current, distY);
        if (scrollTop > 0) {
          reachedTop.current = false;
        } else if (scrollTop === 0 && !reachedTop.current) {
          reachedTop.current = true;
          startY.current = currentY; // 重置起始位置
        } else if (scrollTop === 0 && deltaY < 0) {
          console.log("向上滑动");
        } else if (scrollTop === 0 && reachedTop.current) {
          event.preventDefault(); // 阻止默认滚动行为
          wrapperRef.current.style.transform = `translateY(${distY}px)`;
        }
      } else if (pin === "bottom") {
        event.preventDefault(); // 阻止默认滚动行为
        wrapperRef.current.style.transform = `translateY(${distY}px)`;
      }
    },
    [pin, boudingRect]
  );

  const handleEnd = useCallback(() => {
    if (!moving.current) return;
    moving.current = false;
    const currentTranslateY = getTranslateY();
    const diff = currentTranslateY - originalStartY.current;
    let isChangePin = false;

    // 确保只添加一次事件监听器
    const transitionEndHandler = () => {
      if (isChangePin) {
        console.log("切换pin状态");
        setPin(pin === "bottom" ? "top" : "bottom");
      }
      wrapperRef.current.style.transition = "none"; // 修正过渡属性名称
      wrapperRef.current.removeEventListener(
        "transitionend",
        transitionEndHandler
      );
    };

    wrapperRef.current.addEventListener("transitionend", transitionEndHandler);
    wrapperRef.current.style.transition = "transform 0.3s ease"; // 修正过渡属性名称

    if (pin === "bottom") {
      if (Math.abs(diff) > 100) {
        // 向上滑动超过100px,固定在顶部
        isChangePin = true;
        wrapperRef.current.style.transform = `translateY(${boudingRect[0]}px)`;
      } else {
        // 回到原来的位置
        wrapperRef.current.style.transform = `translateY(${translateY.current}px)`;
      }
    } else if (pin === "top") {
      if (Math.abs(diff) > 100 && reachedTop.current) {
        // 向上滑动超过100px,固定在顶部
        isChangePin = true;
        wrapperRef.current.style.transform = `translateY(${boudingRect[1]}px)`;
      } else {
        // 回到原来的位置
        wrapperRef.current.style.transform = `translateY(${translateY.current}px)`;
      }
    }
  }, [pin, boudingRect]);

  const getTranslateY = () => {
    const transform = window.getComputedStyle(
      document.querySelector(".scroll-area")
    ).transform;
    return parseInt(transform.split(",")[5]);
  };

  useEffect(() => {
    const element = wrapperRef.current;
    if (!element) return;

    console.log("wrapperRef:", element);

    // 使用非被动模式添加 touchmove 事件监听器
    const handleMoveWrapper = (e) => handleMove(e);

    element.addEventListener("touchstart", handleStart);
    // element.addEventListener("mousedown", handleStart);
    window.addEventListener("touchmove", handleMoveWrapper, { passive: false });
    // window.addEventListener("mousemove", handleMoveWrapper, { passive: false });
    window.addEventListener("touchend", handleEnd);
    // window.addEventListener("mouseend", handleEnd);

    return () => {
      element.removeEventListener("touchstart", handleStart);
      // element.removeEventListener("mousedown", handleStart);
      window.removeEventListener("touchmove", handleMoveWrapper, {
        passive: false,
      });
      // window.removeEventListener("mousemove", handleMoveWrapper, {
      //   passive: false,
      // });
      window.removeEventListener("touchend", handleEnd);
      // window.removeEventListener("mouseend", handleEnd);
    };
  }, [handleMove, handleEnd]);

  return (
    <div className="scroll-area" ref={wrapperRef}>
      <div className="scroll-area-bar"></div>
      <div className="scroll-area-content">
        <p>这里是主要内容...</p>
        {/* 添加更多内容以便滚动 */}
        {Array.from({ length: 50 }).map((_, i) => (
          <p key={i} style={{ lineHeight: "24px", fontSize: "14px" }}>
            这是第 {i + 1} 行内容。
          </p>
        ))}
      </div>
    </div>
  );
};

export default ScrollArea;

.scroll-area {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 0;
  height: 100vh;
  z-index: 50;
  background: #fff;
  border-radius: 10px;
  transform: translateY(calc(100% - 64px));
  transition: transform 0.3s ease-in-out;
  overflow: hidden;
}
.scroll-area-bar {
  height: 20px;
  background: #ddd;
  text-align: center;
  color: #eee;
}
.scroll-area-content {
  -webkit-overflow-scrolling: touch;
  height: 100%;
  background: #fafafa;
  padding: 1px 16px;
  overflow-x: auto;
}