JavaScript 实现无缝循环滑动效果

作者:窗边听雨

时间:2026-10-30

浏览:169

本文介绍如何使用 JavaScript 手写实现页面顶部的图片循环滑动效果。通过监听触摸事件计算位移,利用 CSS transform 控制位置,并采用首尾复制法解决边界跳转问题。详细解析了滑动过程中的回弹逻辑、过渡效果控制及边界判断,提供完整代码实现。

文章目录

循环滑动需求背景

近期业务开发中遇到一个常见需求:页面顶部需展示图片列表,支持用户拖动操作。当拖动至最后一张图片时,需实现无缝切换至第一张图片的效果,从而形成循环滑动体验。

此类交互场景在主流应用中十分普遍,例如淘宝、网易云音乐等应用的顶部区域均采用这种滑动模式。

循环滑动需求背景

若无需实现循环滑动功能,仅使用 css 属性即可满足基本需求。

具体实现为:父元素设置固定宽度,并配置 overflow-x:auto 属性。子元素具备各自宽度,横向排列即可正常显示。

然而,一旦要求实现循环滑动,或者无法使用 overflow 属性,实现难度将显著增加。

此时需要手动编写滑动逻辑代码。

触摸事件监听与位移计算

具体步骤包括:1. 监听 touch 相关事件,具体为 touchStart、touchMove 和 touchEnd,通过事件参数计算位移量。

let start = 0, move = 0, end = 0
​​​​​​​    const touchStart = (e) => {
        // 计算滑动的起点,同时需要保存上一次的停留的位置 
        start = e.touches[0].clientX - end
    }
    const touchMove = (e) => {
        // 记录滑动的距离
        move = e.touches[0].clientX - start
    }
    const touchEnd = () => {
        // 记录结束滑动的位置 
        end = move
    }

2. 将计算得到的位移值应用于父元素,例如使用样式代码: transform: translateX(${transform}px)。

<div className={cx('swapper')}
       onTouchStart={touchStart}
       onTouchMove={touchMove}
       onTouchEnd={touchEnd}
       style={{ transform: `translateX(${transform}px)`, transition: transition ? 'transform 0.3s' : 'none' }}
   >
       {
           list.map((item, index) => {
           return <div></div>
           })
        }
        </div>

首尾复制与边界跳转逻辑

当滑动至最后一张图片时,需实现跳转至第一张图片的效果,以达成首尾无缝衔接。

实现方法为:1. 将列表中的第一张图片复制一份,追加到列表末尾;同时将最后一张图片复制一份,插入到列表开头。

if (length > 1) {
    const startItem = list[0]
    const endItem = list[length - 1]
    list.push(startItem)
    list.unshift(endItem)
}

2. 在滑动过程中启用弹性回弹效果。例如,当卡片滑动至一半位置时,需计算其距离最近卡片的节点。假设卡片宽度一致,节点坐标可表示为 [-750, -375, 0, 375] 等值。

const length = list.length
    const boundary = []
    const width = document.body.clientWidth
    for (let i = -length; i <= length - 1; i++) {
        boundary.push(i * width)
    }

在滑动结束时,需判断当前位移距离左侧卡片还是右侧卡片更近。

for (let i = 1; i < boundary.length; i++) {
     // 最后卡片结束的位置在某个区间之内
    if (end > boundary[i - 1] && end < boundary[i]) {
        // 通过减法判断下距离左边还是距离右边更近
        if (end - boundary[i - 1] < boundary[i] - end) {
            end = boundary[i - 1]
            // 设置最终的位置
            setTransform(boundary[i - 1])
        } else {
            end = boundary[i]
            setTransform(boundary[i])
        }
        break
    }
}

过渡效果与回弹控制

回弹动画需要添加过渡效果,但在滑动过程中应移除过渡效果以保证跟手性。因此,在 touchMove 阶段移除 transition 属性,而在 touchEnd 阶段重新开启过渡效果。

滑动结束后,需判断是否到达边界情况:若处于最后一张,需跳转至第一张。需注意,此时跳转不应包含过渡效果,以确保视觉上的无缝连接。

if (end < boundary[0]) {
    end = boundary[0]
    setTransform(end)
}
if (end > boundary[boundary.length - 1]) {
    end = boundary[boundary.length - 1]
    setTransform(end)
}
setTimeout(() => {
    // 关闭过渡效果
    setTransition(false)
    // 如果是第二张,需要跳转到倒数第二张
    if (end < boundary[1]) {
        end = boundary[boundary.length - 2]
        setTransform(end)
    }
    // 如果是倒数第二张,需要跳转到第二张
    // 确保滑动的图片都在中间,而不是在列表的第一张和最后一张
    if (end > boundary[boundary.length - 2]) {
        end = boundary[1]
        setTransform(end)
    }
    // 300 是因为需要等待回弹动画结束
}, 300);

完整代码实现

以下是完整的代码实现示例。

// 必须要放在组件外
let start = 0, move = 0, end = 0
const cx = classBind.bind(styles);
const Component = (props) => {
    const { arr = [] } = props 
    const [list, setList] = useState(arr)
    // 控制位移
    const [transform, setTransform] = useState(0)
    // 控制过渡效果
    const [transition, setTransition] = useState(false)
    // 记录初始位置
    const touchStart = (e) => {
        start = e.touches[0].clientX - end
    }
    // 计算位移
    const touchMove = (e) => {
        move = e.touches[0].clientX - start
        setTransition(false)
        setTransform(move)
    }
    // 结束后计算回弹,和边界情况
    const touchEnd = () => {
        setTransition(true)
        const length = list[0].listLength
        const boundary = []
        const width = document.body.clientWidth
        for (let i = -length; i <= length - 1; i++) {
            boundary.push(i * width)
        }
        end = move
        if (end < boundary[0]) {
            end = boundary[0]
            setTransform(end)
        }
        if (end > boundary[boundary.length - 1]) {
            end = boundary[boundary.length - 1]
            setTransform(end)
        }
        setTimeout(() => {
            setTransition(false)
            if (end < boundary[1]) {
                end = boundary[boundary.length - 2]
                setTransform(end)
            }
            if (end > boundary[boundary.length - 2]) {
                end = boundary[1]
                setTransform(end)
            }
        }, 300);
        for (let i = 1; i < boundary.length; i++) {
            if (end > boundary[i - 1] && end < boundary[i]) {
                if (end - boundary[i - 1] < boundary[i] - end) {
                    end = boundary[i - 1]
                    setTransform(boundary[i - 1])
                } else {
                    end = boundary[i]
                    setTransform(boundary[i])
                }
                break
            }
        }
    }
    useEffect(() => {
            const { length } = list
            list.forEach((item, index) => {
                // 保存真实的位置,因为后面会补充前后两张卡片
                item.activeKey = index
                // 保存真实的列表长度,不直接减2是因为有1张卡片的情况
                item.listLength = length
            })
            if (length > 1) {
                const startItem = list[0]
                const endItem = list[length - 1]
                list.push(startItem)
                list.unshift(endItem)
            }
            setList([...list])
        })
    }, [arr]);
    return <div className={cx('resource-detail')}>
        <div className={cx('swapper')}
            onTouchStart={touchStart}
            onTouchMove={touchMove}
            onTouchEnd={touchEnd}
            style={{ transform: `translateX(${transform}px)`, transition: transition ? 'transform 0.3s' : 'none' }}
        >
            {
                list.map((item, index) => {
                    return <div key={index}>这是卡片</div>
                })
            }
        </div>
    </div>
}

至此,关于 JavaScript 实现手写循环滑动效果的讲解结束。如需了解更多 JavaScript 循环滑动相关内容,建议查阅本站往期文章或继续浏览后续相关文章。感谢大家的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章