Skip to content

封装Xscroll

水平滚动方法一:基础版onwheel

  • 利用onwheel事件的deltaY来直接控制scrollLeft的改变
  • 缺点:各个设备的onwheel事件的触发deltaY是不一致的,浏览器,包括鼠标也可以自己设置,会导致滚动效果一般
html
<head>
<style>
  .wrap {
      position: relative;
      margin: auto;
      width: 400px;
      height: 200px;
      cursor: pointer;
      border: 1px solid #000;
      box-sizing: border-box;
    }

    .scroll {
      width: 400px;
      height: 200px;
      border: 1px solid #f00;
      box-sizing: border-box;
      overflow-x: scroll;
      overflow-y: hidden;
      background: linear-gradient(rgba(122, 122, 50, .3), rgba(20, 200, 150, .3));
      display: flex;
    }

    .item3 {
      width: 200px;
      height: 200px;
      margin-right: 20px;
      border: 1px solid green;
      flex-shrink: 0;
    }

  </style>
</head>

<body>
  <div class="wrap">
    <div class="scroll">
      <div class="item3">1</div>
      <div class="item3">2</div>
      <div class="item3">3</div>
      <div class="item3">4</div>
      <div class="item3">5</div>
      <div class="item3">6</div>
      <div class="item3">7</div>
      <div class="item3">8</div>
      <div class="item3">9</div>
      <div class="item3">10</div>
    </div>
  </div>
  <script>
    const box = document.querySelector('.scroll')
    box.addEventListener('wheel', (e) => {
      e.currentTarget.scrollLeft += e.deltaY;
    })
  </script>
</body>

水平滚动方法二:利用transform来将垂直滚动转换为水平滚动

  • js代码
js
import React, { useEffect, useRef, useState } from 'react';
import style from './xscroll.module.css';
import { useDebounce } from '../../../utils/hooks.js';

const XScroll = React.forwardRef(
    ({ customStyle, children, scrollBar, scrollBarType, contentStyle, containerHeights, selfid = 'defalut' }, ref) => {
        const [wrapperWidth, setWrapperWidth] = useState(0);
        const [wrapperHeight, setWrapperHeight] = useState(0);
        const [scrollbarPosition, setScrollbarPosition] = useState(0)
        const [mouseDownFlag, setMoseDownFlag] = useState(false)
        const [showBar, setShowBar] = useState(scrollBar)
        const wrapperRef = useRef(null);
        const showBarClearId = useRef(null);

        useEffect(() => {
            window.addEventListener('resize', debounceWheel);
            return () => {
                window.removeEventListener('resize', debounceWheel);
            };
        }, []);

        useEffect(() => {
            setShowBar(scrollBar)
        }, [scrollBar])


        const handleResize = () => {
            if (wrapperRef.current) {
                const { width, height } = wrapperRef.current.getBoundingClientRect();
                setWrapperWidth(width);
                setWrapperHeight(height);
            }
        };

        useEffect(() => {
            debounceWheel();
        }, [wrapperRef]);

        const handlerScroll = (e) => {
            if (mouseDownFlag) return
            if (scrollBarType === 'scroll') {
                setShowBar(true)
                clearTimeout(showBarClearId.current)
                showBarClearId.current = setTimeout(() => {
                    setShowBar(false)
                }, 1500);
            }
            const MaxScrollLength = e.target.scrollHeight - e.target.clientHeight
            const assignAbleLength = e.target.clientHeight - 100
            setScrollbarPosition(e.target.scrollTop / MaxScrollLength * assignAbleLength)
        }

        const debounceWheel = useDebounce(handleResize, 300);

        const handlerMoveStart = (event) => { //实现滑动条手动滑动
            setMoseDownFlag(true)
            if (event.button !== 0) return;
            if (scrollBarType === 'scroll') {
                clearTimeout(showBarClearId.current)
            }
            const startX = event.clientX;
            const scrollBox = document.querySelector(`#scrollBox${selfid}`)
            const MaxScrollLength = scrollBox.scrollHeight - scrollBox.clientHeight
            const assignAbleLength = scrollBox.clientHeight - 100
            const maxDiffX = wrapperWidth - 100
            document.onmousemove = function (e) {
                const endX = e.clientX;
                let diffX = endX - startX + scrollbarPosition;
                if (diffX < 0) {
                    diffX = 0
                }
                if (diffX > maxDiffX) {
                    diffX = maxDiffX
                }
                setScrollbarPosition(diffX)
                scrollBox.scrollTo({ top: diffX / assignAbleLength * MaxScrollLength })
            }
            document.onmouseup = function () {
                setMoseDownFlag(false)
                if (scrollBarType === 'scroll') {
                    showBarClearId.current = setTimeout(() => {
                        setShowBar(false)
                    }, 2000);
                }
                document.onmousemove = null;
                document.onmouseup = null;
            }
            if (event.preventDefault) {
                event.preventDefault();
            }
        }

        return (
            <div
                style={customStyle}
                className={style.wrapper}
                ref={wrapperRef}>
                {/* 关键点:
                        1.动态获取父容器的宽高
                        2.设置子容器的宽度为父容器的高度,高度为父元素的宽度
                        3.旋转子容器,注意旋转中心如何改变
                    */}
                <div
                    onScroll={handlerScroll}
                    className={`${style.scrollBox} ${showBar ? style.showBar : style.hideBar
                        }`}
                    //天坑,generate和prod页面刚生成的时候是覆盖的,所以直接拿id拿的是generate中的Xscroll,导致滑动失效
                    id={"scrollBox" + selfid}
                    ref={ref}
                    style={
                        width: containerHeights ? containerHeights + 7 : wrapperHeight,
                        height: wrapperWidth,
                        transform: `translateY(${containerHeights ? containerHeights : wrapperHeight}px) rotate(-90deg)`,
                    }
                >
                    <div
                        className={style.content}
                        style={
                            contentStyle === 'widthAuto'
                                ? {
                                    width: wrapperWidth,
                                    height: 'auto',
                                    left: containerHeights ? containerHeights : '100%'
                                }
                                : {
                                    width: '100%',
                                    height: 'fit-content',
                                    left: containerHeights ? containerHeights : '100%'
                                }
                        }
                    >
                        {wrapperRef.current && children}
                    </div>
                </div>
                <div
                    onMouseDown={handlerMoveStart}
                    className={style.scrollBar}
                    style={showBar ? { left: scrollbarPosition } : { display: 'none' }}>
                </div>
            </div>
        );
    },
);

XScroll.displayName = "XScroll"
export { XScroll };
  • css代码
css
.wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  cursor: pointer;
  box-sizing: border-box;
}

.scrollBox {
  position: relative;
  box-sizing: border-box;
  transform-origin: 0 0;
  overflow: auto;
  /* 隐藏水平滚动轴 显示竖直滚动轴 */
  overflow-x: hidden;
  direction: rtl;

}

.content {
  position: absolute;
  /* left: 70%; */
  /* 宽度自动,高度和最顶层的宽度一致 */
  box-sizing: border-box;
  transform-origin: 0 0;
  transform:rotate(90deg);
  display: flex;
  direction: ltr;
}

.scrollBar{
  position: absolute;
  bottom: -10px;
  background-color:#c5c5c5 ;
  width: 100px;
  height: 7px;
  border-radius: 5px;

}

方法三:onwheel进阶版

  • 利用onwheel + transition 模拟水平滚动
  • js代码实现
js
const XsctollEasy = ({ children }) => {

    const stylizedContent = useRef(null)
    const xscollContent = useRef(null)
    const [deltaY, setDeltaY] = useState(0);

    const handlerWheel = (e) => {    
      const speed = 40  //自己控制滑动速度
      const pWidth = stylizedContent.current?.clientWidth
      const sWidth = xscollContent.current?.clientWidth
      if (sWidth <= pWidth) return
      if (e.deltaX > 0) { //deltaX兼容触摸板左右滑动实现水平滚动
        if (-deltaY < sWidth - pWidth) {
          setDeltaY(pre => pre - speed)
        }
      } else if (e.deltaX < 0) {
        if (deltaY < 0) {
          setDeltaY(pre => pre + speed)
        }
      } else if (e.deltaY > 0) {
        if (-deltaY < sWidth - pWidth) {
          setDeltaY(pre => pre - speed)
        }
      } else if (e.deltaY < 0) {
        if (deltaY < 0) {
          setDeltaY(pre => pre + speed)
        }
      }
    }
    return (
      <div
        className={style.stylizedContent}
        ref={stylizedContent}
        onWheel={handlerWheel}
      >
        <div
          className={style.xscollContent}
          ref={xscollContent}
          style={
            transform: `translateX(${deltaY}px)`
          }>
          {children}
        </div>
      </div>
    )
  }
  • css代码
css
.stylizedContent {
    width: 100%;
    clip-path: inset(0px 0px -100px 0px);  /* 根据情况来控制裁剪*/
}

.xscollContent {
    width: fit-content;
    display: flex;
    transition: transform 0.2s ease-out;   /* 自己控制过渡效果  ease-out滑动后续会减速*/
    gap: 8px;
}

兼容平板等触摸设备的滑动

  • 利用touch事件计算滑动量再派发onwhell事件
js
useEffect(() => {
    const scrollBox = stylizedContent.current;
    let startX;
    let startScrollTop;
    // 兼容触屏滑动
    const touchStart = (event) => {
      startX = event.touches[0].pageX;
      startScrollTop = scrollBox.scrollTop;
    };
    const touchMove = (event) => {
      const currentX = event.touches[0].pageX;
      const diffX = startX - currentX;
      scrollBox.scrollTop = startScrollTop + diffX;

      // 创建并触发一个模拟的wheel事件
      const wheelEvent = new WheelEvent('wheel', {
        deltaX: diffX, // 模拟横向滑动的位移作为deltaX
        deltaY: 0, // Y方向位移为0
        clientX: currentX,
        clientY: event.touches[0].clientY,
        bubbles: true,
        cancelable: true,
      });
      scrollBox.dispatchEvent(wheelEvent);
      event.preventDefault();
    };
    const wheel = (event) => {
      if (event.deltaX) {
        event.preventDefault(); // 防止左右回退
        scrollBox.scrollTop += event.deltaX;
      }
    };
    scrollBox.addEventListener('touchstart', touchStart);
    scrollBox.addEventListener('touchmove', touchMove);
    scrollBox.addEventListener('wheel', wheel);
    return () => {
      scrollBox.removeEventListener('touchstart', touchStart);
      scrollBox.removeEventListener('touchmove', touchMove);
      scrollBox.removeEventListener('wheel', wheel);
    };
  }, []);