React 实现鼠标水平滚动组件

实现要点

  • 页面布局
  • 监听鼠标滚动事件
  • 计算滚动位置进行对齐

实现步骤

页面布局

  • 父元素采用flex布局且设置flex-wrap: nowrap使其子元素可以完全展开
  • 子元素设置flex-shrink: 0使其能够不进行自适应缩小

事件监听

  • 通过调用event.preventDefault()阻止浏览器默认行为
  • 使用useRef()获取父元素的DOM元素,使用.current获取dom对象进行操作
  • 设置父元素的wheel鼠标滚动监听事件,并进行对应的计算

注意事项

  • 使用react onWheel事件进行阻止默认行为无效,且会提示报错,所以使用ref获取dom元素代替
  • react 事件是合成事件且不持久,不可异步传入

元素滚动

  • 元素可以通过scrollTo()方法进行滚动

  • Tips:

    • offsetWidth/offsetHeight 获取元素宽高
    • scrollLeft/Top 获取偏移位置
    • scrollWidth 获取滚动宽度

参考代码

import { createStyles, withStyles } from '@material-ui/core/styles'
import { SitePropsType } from 'components/base/Site'
import { useEffect, useRef } from 'react'

const styles = createStyles({
  root: {
    overflowX: 'auto',
  },
  container: {
    display: 'flex',
    flexWrap: 'nowrap',
    overflowX: 'auto',
  },
  item: {
    height: '300px',
    width: '100%',
    backgroundColor: '#f0f0f0',
    border: '1px solid #333333',
    flexShrink: 0,
    // '&:hover': {
    //   cursor: 'pointer',
    // },
  },
  indicator: {},
})

interface SiteSwiperProps {
  classes?: {
    root: string
    container: string
    item: string
    indicator: string
  }
  sites: SitePropsType[]
  row?: number
}

/**
 * 计算滚动位置
 * @param currentScrollLeft
 * @param scrollElWith
 */
const computeScroll = (
  currentScrollLeft: number,
  scrollElWith: number
): number => {
  // 判断滚动偏移是否满足滚动要求
  console.log('current scroll left:', currentScrollLeft)
  const index = Math.round(currentScrollLeft / scrollElWith)
  return scrollElWith * index
}

function SiteSwiper({ classes, sites, row = 3 }: SiteSwiperProps): JSX.Element {
  const containerRef = useRef(null)
  const timer = useRef(null)

  useEffect(() => {
    console.log('current ref:', containerRef)
    containerRef.current.addEventListener('wheel', (e) => {
      console.log('mouse wheel event:', e)
      // 阻止原生滚动事件
      e.preventDefault()

      // 获取滚动位置
      let scrollLeft = containerRef.current.scrollLeft
      const scrollTotalWidth = containerRef.current.scrollWidth
      const scrollItemWidth = containerRef.current.offsetWidth

      // 获取容器的宽度
      console.log(
        'current container:',
        containerRef.current.offsetWidth,
        e.deltaY
      )
      // 即时水平滚动偏移值
      const bufferOffset = 70
      const scrollBehavior = 'smooth'
      let offset = scrollLeft + e.deltaY * 4 // 放大偏移倍数
      if (offset >= scrollTotalWidth - scrollItemWidth + bufferOffset) {
        // 到达最后元素
        offset = offset - scrollTotalWidth - bufferOffset
        // scrollBehavior = 'auto'
      } else if (offset + bufferOffset < 0) {
        // 达到第一元素
        offset = scrollTotalWidth + offset - bufferOffset
        // scrollBehavior = 'auto'
      } else {
        // 其它情况
      }
      console.log('offset y at time:', scrollLeft, offset)
      containerRef.current.scrollTo({
        top: 0,
        left: offset,
        behavior: scrollBehavior,
      })

      // 防抖
      if (timer.current) {
        clearTimeout(timer.current)
      }

      timer.current = setTimeout(() => {
        // 计算滚动最后的位置进行位置矫正
        console.log('TIME OUT: starting position correct...')
        // 计算是否滚动
        scrollLeft = computeScroll(offset, scrollItemWidth)

        containerRef.current.scrollTo({
          top: 0,
          left: scrollLeft,
          behavior: 'smooth',
        })
      }, 700)
    })
  })

  return (
    <div className={classes.root} id="swiper-container">
      {/* Content */}
      <div
        className={classes.container}
        // onScroll={handleMouseScroll}
        // onMouseOver={handleMouseOver}
        // onWheel={handleWheel}
        ref={containerRef}
      >
        {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
          <div className={`${classes.item} swiper-item`} key={item}>
            {item}
          </div>
        ))}
      </div>

      {/* Indicator */}
      <div className={classes.indicator}></div>
    </div>
  )
}

export default withStyles(styles)(SiteSwiper)

posted @ 2021-03-13 18:26  冬天之歌  阅读(903)  评论(0编辑  收藏  举报