图片轮播

Clloz · · 1,156次浏览 ·

前言

用两种方式实现图片轮播,自动轮播和拖动轮播。

代码结构

由于 img 是一个可选标签,可以被拖动,我们这里选择用 background-image 来实现。基本的 DOM 结构和 CSS 如下:

<style>
    .carousel {
        width: 500px;
        height: 280px;
        margin: 30px auto;
        font-size: 0;
        white-space: nowrap;
        overflow: hidden;
    }
    .carousel > div {
        display: inline-block;
        width: 500px;
        height: 280px;
        background-size: contain;
        transition: ease 0.5s;
    }
    .carousel > div:nth-child(1) {
        background-image: url('https://img.clloz.com/blog/writing/cat1.jpg');
    }
    .carousel > div:nth-child(2) {
        background-image: url('https://img.clloz.com/blog/writing/cat2.jpg');
    }
    .carousel > div:nth-child(3) {
        background-image: url('https://img.clloz.com/blog/writing/cat3.jpg');
    }
    .carousel > div:nth-child(4) {
        background-image: url('https://img.clloz.com/blog/writing/cat4.jpg');
    }
</style>

<div class="carousel cp1">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
</div>
<div class="carousel cp2">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
</div>

我们用 display: inline-blockoverflow: hidden 让图片横向排列并且一次只显示一张。这里需要注意 inline-block 空格导致的间隙问题,我直接用 font-size: 0 来解决。

自动轮播

自动轮播的逻辑是比较简单的,我们其实只要关注两张图片,即当前图片和下一张图片。设每一张图片的编号为 index (从 0 开始),每一张图片要显示所对应的 translateX 的值就是 -(index * 100)%。所以我们只需要每次移动时将当前图片和下一章图片左移一个单位(即一张图片的宽度),同时将下一次要显示的图片放到当前图片的右边一个单位。如下图:

carousel

关键逻辑就是将下次要显示的图片移动到预备位置,并且这个过程要关掉动画效果。最后的代码如下:

// transform loop settimeout
let el2 = document.querySelector('.carousel.cp2');
let currentIndex = 0;
setInterval(() => {
    let children = el2.children;
    let nextIndex = (currentIndex + 1) % children.length;
    let current = children[currentIndex];
    let next = children[nextIndex];

    next.style.transition = 'none';
    next.style.transform = `translateX(${100 - nextIndex * 100}%)`;

    setTimeout(() => {
        next.style.transition = '';
        current.style.transform = `translateX(${-100 - currentIndex * 100}%)`;
        next.style.transform = `translateX(${-nextIndex * 100}%)`;
        currentIndex = nextIndex;
    }, 16);
}, 3000);

动画效果的开关我们可以用 element.style.transition 来控制行内样式的值来达到效果。因为行内样式的优先级最高,当我们设置其值为 none 会覆盖 style 标签中的样式。当我们将 element.style.transition 的值设为 ''style 标签中的对应样式又生效了

代码中间使用了一个小技巧。就是如果连续对同一个元素进行操作,浏览器会忽略前一个操作,这里我们用一个 setTimeout 避免浏览器的这种行为,16ms 为浏览器一帧的时间。

还有一点就是最后一张图片的下一张应该是第一张,这里我们可以使用简单的模运算来达到效果,模就是元素的个数。

拖动轮播

拖动轮播的逻辑要比自动复杂一些,因为拖动的情况下我们既可以向左又可以向右进行拖动。并且当拖动结束的时候,显示窗口中会有两张图片,我们要根据面积来判断哪张图片显示,并且要把另一张图片也移动到窗口外。

首先我们要处理拖动事件,这个逻辑和我在拖动旋转 3D 的骰子效果一文中的逻辑是相同,我们对包含元素,也就是代码中的 .carousel 绑定一个 mousedown 事件,在其回调函数中绑定 mousemovemouseup 事件。注意后两个事件要绑定到 document 上,因为我们即使拖动到元素外也是一个完整的 mousemove 行为,并且绑定到 document 上即使我们拖出浏览器外也依然能保持触发 mousemove 事件。

mousedown 事件我们只要做一件事就是记录用户点击的初始位置坐标,用 clientX(因为轮播是横向的,我们只需要判断 x 方向的移动距离)

mousemove 我们要处理图片的移动,这里我们需要将动画效果关闭。以一张图片的的宽度为一个单位,我们只需要知道一共滚过了几个单位,就知道当前显示图片的 index,然后在计算当前 index 的前一张和后一张就能够得到连续的效果。mousemove 回调函数如下:

let move = e => {
    let x = e.clientX - startX;

    //拖动的整数屏
    let current = index - (x - (x % 500)) / 500;

    for (let offset of [-1, 0, 1]) {
        //计算可能出现的图片下标
        let pos = current + offset;
        pos = (pos + children.length) % children.length; //要处理大屏幕拖动小于 -children.length 的情况

        children[pos].style.transition = 'none';
        children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)`; //当前,前一个,后一个图片当前位置
    }
};

mouseup 是这里面逻辑稍微复杂的一个,当我们拖动停止的时候,视口里面会有两张图片(一般情况下)(xe.clientX - startX,即从触发 mousedownmouseup 水平方向一共移动了多少像素),我们要判断哪张图片所占面积比较大,让这张图片用动画移动到整个视口,而另一张图片用动画移出视口。这里我讲两种实现方式。

第一种是比较好理解,但是代码量比较大。我们可以用 (x - (x % 500)) / 500 得出一共滚动了多少个单位,index - (x - (x % 500)) / 500 这张图片在触发 mouseup 时必然在视口内,只是我们不确定它是要移出的还是要显示的,我们只需要分情况,用 if 判断一下 x % 500 的各种情况。

x % 500 的值就是页面滚动完整数个单位后多的部分,这部分如果超过一半的图片宽度,当前图片就要从视口移出;如果小于一半的图片宽度,当前图片就显示到视口中。同时我们还要判断视口中的另一张图片的移动方向,这里就不仅需要判断面积,同时需要判断是向左拖动还是向右拖动的。所以最后我们要 2 x 2 共四种情况。代码如下:

let up = e => {
    let x = e.clientX - startX;

    //index不变,向下取整,需要分情况,可读性好
    index = (index - (x - (x % 500)) / 500) % children.length;
    index = (index + children.length) % children.length;
    let base = x % 500;

    if (base > 0) {
        if (base > 250) {
            children[index].style.transition = '';
            children[index].style.transform = `translateX(${(-index + 1) * 500}px)`;
            let pre = index === 0 ? children.length - 1 : index - 1;
            children[pre].style.transition = '';
            children[pre].style.transform = `translateX(${-pre * 500}px)`;
            index = pre;
        } else {
            children[index].style.transition = '';
            children[index].style.transform = `translateX(${-index * 500}px)`;
            let pre = index === 0 ? children.length - 1 : index - 1;
            children[pre].style.transition = '';
            children[pre].style.transform = `translateX(${(-pre - 1) * 500}px)`;
        }
    }
    if (base < 0) {
        if (base < -250) {
            children[index].style.transition = '';
            children[index].style.transform = `translateX(${(-index - 1) * 500}px)`;
            let pre = index === 3 ? 0 : index + 1;
            children[pre].style.transition = '';
            children[pre].style.transform = `translateX(${-pre * 500}px)`;
            index = pre;
        } else {
            children[index].style.transition = '';
            children[index].style.transform = `translateX(${-index * 500}px)`;
            let pre = index === 3 ? 0 : index + 1;
            children[pre].style.transition = '';
            children[pre].style.transform = `translateX(${(-pre + 1) * 500}px)`;
        }
    }

    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', up);
};

上面的代码比较繁琐,其实我们可以将情况简化,我们可以利用 Math.round() 四舍五入求出 mouseup 触发以后要显示的图片(四舍五入后就不需要在特别判断哪个占的面积大了),现在我们要判断的就是另一张图片是前一张还是后一张即可。这个判断也是有规律的,我们利用 Math.abs()Math.sign() 可以得出其值,Math.sign(base) 判断是向左还是向右拖动,Math.abs(base) 判断 base 是否超过一半,结合两者我们就能判断出是上一张还是下一张。代码如下:

let up = e => {
    let x = e.clientX - startX;

    //index 四舍五入,代码简洁,不易理解,主要是利用四舍五入,统一了要从可视范围移出的元素的下标
    index = (index - Math.round(x / 500)) % children.length;
    index = (index + children.length) % children.length; //四舍五入,得到的就是mouseup触发后应该显示的图片下标
    let base = x % 500;
    for (let offset of [0, (Math.abs(base) > 250 ? 1 : -1) * Math.sign(base)]) {
        let pos = (index + offset + children.length) % children.length; //获得另一个要移动的图片的下标(要移除可视范围的图片)
        children[pos].style.transition = '';
        children[pos].style.transform = `translateX(${(offset - pos) * 500}px)`; //一个下标为index图片要显示它的偏移量是 -index, 偏移量 -1 表示再向左移动一个图片单位,偏移量 1 表示向右移动一个图片单位,最后的总偏移量为 -index + offset
    }

    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', up);
};

这样代码也简单多了,不过要比上面那个难理解一点。理解的关键就是,同一个方向的移动,base 是否超过一半其 index 是不同的。

完整的代码查看:图片轮播效果

总结

轮播问题的主要逻辑就是我们不需要关注所有图片,不需要每次移动都要保持视口外的图片都在“正确”的位置,我们只需要关注几张与当前显示相关的图片即可。


Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

 

00:00/00:00