JavaScript实现手写循环滑动效果

 更新时间:2023年05月23日 14:42:33   作者:然燃  
最近一直在做业务,遇到一个需求是页面顶部需要展示图片,可以拖动,拖动到最后一张的时候需要无缝切换到第一张,从而实现循环滑动,所以本文就来和大家分享一下实现方法

最近一直在做业务,遇到一个需求是: 页面顶部需要展示图片,可以拖动,拖动到最后一张的时候需要无缝切换到第一张,可以实现循环滑动。

这种需求场景挺常见的,比如淘宝,网易云,页面顶部都是这个模式:

如果不要求循环滑动,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循环滑动内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论