惯性滚动

22 阅读3分钟

页面滑动太过生硬可以使用惯性滚动的模式,向下滚动一段距离慢慢停止,交互效果更好


export function useSmoothScroll() {
  let rafId = null
  let isScrolling = false
  let currentScrollY = 0
  let velocity = 0
  let lastScrollY = 0
  let lastTime = 0
  let isWheeling = false
  let wheelTimeout = null

  const smoothScroll = () => {
    if (!isScrolling) return

    const currentTime = performance.now()
    const rawDeltaTime = currentTime - lastTime
    const deltaTime = Math.min(rawDeltaTime, 32) / 16.67 // 归一化到60fps,限制最大deltaTime
    lastTime = currentTime

    // 获取滚动边界
    const maxScroll = document.documentElement.scrollHeight - window.innerHeight
    const minScroll = 0

    // 检查是否到达边界
    const atTop = currentScrollY <= minScroll
    const atBottom = currentScrollY >= maxScroll

    // 如果到达边界,立即停止并修正位置
    if (atTop || atBottom) {
      // 如果速度方向是朝向边界外的,立即停止
      if ((atTop && velocity < 0) || (atBottom && velocity > 0)) {
        velocity = 0
      }
      
      // 修正位置到边界
      currentScrollY = atTop ? minScroll : maxScroll
      window.scrollTo(0, currentScrollY)
      
      // 如果速度很小,停止滚动
      if (Math.abs(velocity) < 0.05) {
        isScrolling = false
        velocity = 0
        return
      }
    }

    // 如果速度很小,停止滚动(降低阈值,让滚动持续更久)
    if (Math.abs(velocity) < 0.05) {
      window.scrollTo(0, currentScrollY)
      isScrolling = false
      velocity = 0
      return
    }

    // 只使用摩擦力让速度自然衰减(移除弹簧效果)
    const friction = 0.95 // 摩擦力,控制惯性衰减速度(值越大衰减越慢,滚动距离更远)
    
    // 更新速度(只衰减,不添加朝向目标的加速度)
    velocity = velocity * friction
    
    // 更新位置
    currentScrollY += velocity * deltaTime
    
    // 限制滚动范围(防止超出边界)
    currentScrollY = Math.max(minScroll, Math.min(currentScrollY, maxScroll))
    
    // 如果超出边界,立即停止速度
    if (currentScrollY <= minScroll && velocity < 0) {
      velocity = 0
      currentScrollY = minScroll
    }
    if (currentScrollY >= maxScroll && velocity > 0) {
      velocity = 0
      currentScrollY = maxScroll
    }
    
    window.scrollTo(0, currentScrollY)
    
    rafId = requestAnimationFrame(smoothScroll)
  }

  const handleWheel = (e) => {
    e.preventDefault()
    
    const delta = e.deltaY
    const scrollSpeed = 1.2 // 增加滚动速度系数
    
    // 获取当前滚动位置和边界
    const currentPos = window.scrollY
    const maxScroll = document.documentElement.scrollHeight - window.innerHeight
    const minScroll = 0
    
    // 检查是否已经在边界
    const atTop = currentPos <= minScroll
    const atBottom = currentPos >= maxScroll
    
    // 如果在边界且滚动方向是朝向边界外,不处理
    if ((atTop && delta < 0) || (atBottom && delta > 0)) {
      velocity = 0
      return
    }
    
    // 直接给速度一个增量,而不是设置目标位置
    // 这样滚动会自然衰减,不会像弹簧一样
    const currentTime = performance.now()
    const timeDelta = currentTime - lastTime || 16
    
    // 计算速度增量(增加系数以获得更大的滚动距离)
    const velocityIncrement = (delta * scrollSpeed) / timeDelta * 1.0
    
    // 累加速度(可以累积多次滚动的速度)
    velocity += velocityIncrement
    
    // 增加最大速度限制,允许滚动更远
    const maxVelocity = 30
    velocity = Math.max(-maxVelocity, Math.min(velocity, maxVelocity))
    
    lastTime = currentTime
    
    // 标记正在滚动
    isWheeling = true
    
    // 清除之前的超时
    if (wheelTimeout) {
      clearTimeout(wheelTimeout)
    }
    
    // 如果停止滚动一段时间,开始惯性滚动
    wheelTimeout = setTimeout(() => {
      isWheeling = false
    }, 50)
    
    if (!isScrolling) {
      isScrolling = true
      currentScrollY = window.scrollY
      lastScrollY = window.scrollY
      rafId = requestAnimationFrame(smoothScroll)
    }
  }

  const handleTouchStart = (e) => {
    lastScrollY = window.scrollY
    lastTime = performance.now()
    velocity = 0
    isWheeling = false
  }

  const handleTouchMove = (e) => {
    const currentTime = performance.now()
    const timeDelta = currentTime - lastTime
    
    const currentPos = window.scrollY
    const maxScroll = document.documentElement.scrollHeight - window.innerHeight
    const minScroll = 0
    
    // 检查是否在边界
    const atTop = currentPos <= minScroll
    const atBottom = currentPos >= maxScroll
    
    if (timeDelta > 0) {
      const scrollDelta = currentPos - lastScrollY
      
      // 如果在边界且滚动方向是朝向边界外,不更新速度
      if ((atTop && scrollDelta < 0) || (atBottom && scrollDelta > 0)) {
        velocity = 0
      } else {
        // 直接设置速度,不使用目标位置
        velocity = scrollDelta / timeDelta * 0.5 // 触摸滚动惯性系数
      }
    }
    
    lastScrollY = currentPos
    lastTime = currentTime
    currentScrollY = currentPos
    
    if (!isScrolling) {
      isScrolling = true
      rafId = requestAnimationFrame(smoothScroll)
    }
  }

  const handleTouchEnd = () => {
    // 触摸结束后,确保位置在有效范围内
    const maxScroll = document.documentElement.scrollHeight - window.innerHeight
    const currentPos = window.scrollY
    
    // 如果超出边界,立即修正
    if (currentPos < 0) {
      window.scrollTo(0, 0)
      currentScrollY = 0
      velocity = 0
    } else if (currentPos > maxScroll) {
      window.scrollTo(0, maxScroll)
      currentScrollY = maxScroll
      velocity = 0
    }
    
    // 如果速度方向朝向边界外,停止速度
    if ((currentPos <= 0 && velocity < 0) || (currentPos >= maxScroll && velocity > 0)) {
      velocity = 0
    }
  }

  onMounted(() => {
    // 初始化
    currentScrollY = window.scrollY
    lastScrollY = window.scrollY
    lastTime = performance.now()
    
    // 使用被动监听器以提高性能(wheel需要非被动以preventDefault)
    window.addEventListener('wheel', handleWheel, { passive: false })
    window.addEventListener('touchstart', handleTouchStart, { passive: true })
    window.addEventListener('touchmove', handleTouchMove, { passive: true })
    window.addEventListener('touchend', handleTouchEnd, { passive: true })
  })

  onUnmounted(() => {
    window.removeEventListener('wheel', handleWheel)
    window.removeEventListener('touchstart', handleTouchStart)
    window.removeEventListener('touchmove', handleTouchMove)
    window.removeEventListener('touchend', handleTouchEnd)
    
    if (rafId) {
      cancelAnimationFrame(rafId)
    }
    
    if (wheelTimeout) {
      clearTimeout(wheelTimeout)
    }
  })
}


使用,放到触发全局滚动条的位置,出现滚动条就可以应用, const friction = 0.95 // 摩擦力,控制惯性衰减速度(值越大衰减越慢,滚动距离更远)滚动的丝滑程度可以通过滑动系数来控制,值越大摩擦力越小

import { useSmoothScroll } from './composables/useSmoothScroll'

// 启用惯性滚动
useSmoothScroll()