从原理上理解,如何利用 CSS3 transition 打造无限轮播图

始发于个人博客 ryougifujino.com,欢迎访问留言。javascript

以前翻译了两篇有关CSS动画的文章,这篇正好能够用来复习,这是写这篇文章的主要目的。同时,也将从原理上讲解如何从零打造一个无限轮播图,这个过程并不会很复杂。css

轮播图的实现方式有不少种,好比有经过 js 控制marginLeft或者left,经过在一段时间内不断改变它们的值,来达到实现动画的效果,可是都9102年了,这种方法不但不利于浏览器优化,并且平移的动画是线性的,并不美观。而使用CSS3的transition能够很好的解决这两个问题,而且代码还要更少,并且它的兼容性还很不错,稍微现代一点的浏览器都是没有问题的。html

本文默认你已经了解了transition的相关知识,若是你还没了解,请参考这篇文章java

轮播图的原理

首先,咱们来看一下最终的效果。从中能够看到此例的轮播图有如下几个特色(出于简洁的考虑省略了导航点的实现):git

  1. 当鼠标没有置于其上时,它会自动向右无限循环滚动。
  2. 当鼠标置于其上时,滚动会中止。
  3. 点击两边的按钮,能够切换上一页和下一页。
  4. 它的过渡动画是非线性的。

有了一个直观的认识后,咱们来简单地描述一下它的原理。github

全部轮播图的实现方式的内核都是差很少的,主要分红三个部分:最外层容器,全部页的容器和每一页。浏览器

<div class="container">
    <div class="pages">
        <div class="page" style="background: lightgreen;">1</div>
        <div class="page" style="background: lightblue;">2</div>
        <div class="page" style="background: lightgray;">3</div>
    </div>
</div>
复制代码

最外层容器 container 和 page 的长宽是彻底相等的,这样每次滚动后咱们刚好能够显示一个 page,这是精髓所在。而每一页的容器 pages 的宽度则是全部 page 的宽之和,长度和 page 也同样,它仅仅是一个把全部 page 装在一行上的一个长长的容器。下一步咱们给 container 设置overflow-x: auto,因为 pages 的宽是 container 的三倍,全部这时候 container 出现了滚动条,咱们能够左右滚动。能够发现,这时候和最终形态已经很像了,可是真正的轮播图是没有滚动条的,因此咱们要改成overflow-x: hidden,而且使用必定的手段来让它自动滚动。能够看到,本质的原理就是如此的简单。bash

轮播图的实现

下面来看一下上面出现的 CSS 是怎样的。app

首先是容器,咱们在这里给它加了一个边框来让它更明显一点。函数

.container {
  width: 600px;
  height: 300px;
  border: 1px solid black;
  overflow-x: hidden;
}
复制代码

而后是每一页的容器,在这里并无设置width,它是经过后续的 js 代码来设置的。

.pages {
  height: 300px;
}

复制代码

最后是每一页的 class,由于要让每一个 page 在 pages 中并排排列,因此咱们要为它设置浮动(固然使用flex也是能够的)。

.page {
  width: 600px;
  height: 300px;
  float: left;
}
复制代码

好了,样式设置完成以后看上去已经有模有样,除了不会动之外:)。下面咱们来考虑如何让它动起来。

目前咱们看到是浅绿色的第一页,下一步确定是想让它变更到浅蓝色(第二页)上面去。很容易就想到能够经过设置 pages 的marginLeft来实现。获取 pages 如今的marginLeft(固然如今是0),而后再让它减去一个 page 的宽度,这时候刚好显示的不就是第二页了吗?以后,咱们再使用一个定时器来自动调用这个过程,看起来轮播图就完成了。

const to = parseInt(pagesStyle.marginLeft) - PAGE_WIDTH;
translatePage(to);
复制代码

可是这样只能实现单向滚动,而咱们最终须要的效果是无限循环滚动,因此在移动页面函数translatePage中要作一个处理:当发现已经移动到末尾时,咱们就须要把marginLeft从新置为开始的位置,而后再进行移动,这样至少看上去已是循环的了。

目前代码实现的移动效果是生硬的,没有过渡效果。因此咱们要使用transition来使得这个过程变得天然。

.page {
  height: 300px;
  transition: margin-left 0.5s;
}
复制代码

只须要新增短短的一行代码,就实现了非线性的过渡效果,非常不错。

可是这个时候,咱们又发现了一个问题,在从最后一页返回到第一页的时候,过渡效果再也不是从右向左,而是相反的。因此咱们要使用一个小小的技巧,在最后一页添加一个克隆的第一页,这样至少从视觉上,咱们能够看到最后一页过渡到第一页时的从左向右的过渡效果,在下一次移动时,咱们悄悄的将它以无过渡动画的形式先移动到视觉上的第一页,而后再以有过渡动画的形式从视觉上的第一页移动到视觉上的第二页。

<div class="container">
    <div class="pages">
        <div class="page" style="background: lightgray;">3</div>
        <div class="page" style="background: lightgreen;">1</div>
        <div class="page" style="background: lightblue;">2</div>
        <div class="page" style="background: lightgray;">3</div>
        <div class="page" style="background: lightgreen;">1</div>
    </div>
</div>
复制代码

这里能够看到,在第一页前面也插入了克隆的最后一页,道理其实相似,是为了产生欺骗性的从第一页滚动到最后一页时的从右向左的过渡效果。

在这里咱们并不直接在 HTML 代码中添加这两个 page,由于这不是很利于维护。咱们使用 js 代码来动态生成这两页:

const pages = document.querySelector('.pages');
const firstPage = pages.firstElementChild;
const lastPage = pages.lastElementChild;
// 在 pages 容器的首尾分别添加第一页和最后一页
pages.insertBefore(lastPage.cloneNode(true), firstPage);
pages.appendChild(firstPage.cloneNode(true));
复制代码

好了,如今咱们来看看最关键的translatePage函数,它是用来滚动咱们页面的核心函数。其做用就是,把某页滚动到下一页或者上一页。当发现当前页是最后一页时,会先以无动画的形式滚动到视觉上的第一页(实际上是第二页),而后再以动画的形式滚动到第二页;当发现当前页是第一页时,运做同理。实现了这个函数,咱们再经过定时器或者上一页下一页的按钮来调用它,整个轮播图基本上就算完成了。

function translatePage(to) {
  const nowPosition = parseInt(pagesStyle.marginLeft);
  if (to < nowPosition) {
    // 从左向右移动
    if (Math.abs(to) > (PAGES_WIDTH - PAGE_WIDTH)) {
      const newPosition = -PAGE_WIDTH;
      _translatePage(newPosition, true);
      to = newPosition - PAGE_WIDTH;
    }
    play(() => _translatePage(to));
  } else if (to > nowPosition) {
    // 从右向左移动
    if (to > 0) {
      const newPosition = -PAGES_WIDTH + (2 * PAGE_WIDTH);
      _translatePage(newPosition, true);
      to = newPosition + PAGE_WIDTH;
    }
    play(() => _translatePage(to));
  } else {
    // 不动,什么也不作
  }
}
复制代码

这里的to,固然指的就是目标位置的marginLeft,能够看出这里和当前的marginLeft作了一个比较,从而判断出方向。从左向右移动的状况中,Math.abs(to) > (PAGES_WIDTH - PAGE_WIDTH)表示目标位置的marginLeft超过了最后一页的marginLeft,可见这时候是一个新的循环了,因此要以无动画(_translatePage(newPosition, true),第二个参数是无动画的意思)的形式移动到视觉上的第一页。而后改变to,再移动到视觉上的第二页。从右向左移动的状况同理。

如何实现将有动画滚动变为无动画滚动的呢?其实将 CSS 中的transition重置就能够了。

.immediate {
  transition: none;
}
复制代码
function _translatePage(to, immediate) {
  pages.className = 'pages' + (immediate ? ' immediate' : '');
  pages.style.marginLeft = to + 'px';
}
复制代码

可是这里依然有个问题,因为对 class 添加immediate(做用是取消transition)和设置marginLeft以后,样式须要从新计算(这须要时间),若是咱们再这里不使用play方法而是当即调用_translatePage(to),就会发现整个过程看上去就像是从最后一页从右向左过渡到了视觉上的第二页,这是因为以前的样式还没计算完成咱们就将它覆盖了。因此咱们要确保以前的样式生效以后,再进行视觉上的第一页到第二页的过渡。那么如何确保呢?答案就是使用requestAnimationFrame

function play(callback) {
  window.requestAnimationFrame(() => 
    window.requestAnimationFrame(callback));
}
复制代码

这里为何要使用两次requestAnimationFrame呢?由于requestAnimationFrame的回调表示它会在文档下一次重绘以前执行。问题在于,由于是发生重绘前的,因此样式的重计算尚未真的发生。因此须要调用第二次,这时候第一次重绘已经执行(也就表示样式已经发生了重计算),也就是说目前已经处于视觉上的第一页,咱们再变化到视觉上的第二页时过渡就正常了。

能够参考一下这篇文章,里面讲到让动画再次运行起来的时候也谈到了这个问题,可是注意animationtransition稍有不一样,连续设置含有transition的 class 是能够覆盖的,而含有animation的 class 连续设置是不能覆盖的。

核心函数已经实现完成了,接下来咱们让它自动滚起来,这就很是简单了。

let timer;
const run = () => {
    timer = setTimeout(() => {
        const to = parseInt(pagesStyle.marginLeft) - PAGE_WIDTH;
        translatePage(to);
        run();
    }, TRANSLATION_DELAY);
};
run();
复制代码

咱们只须要利用setTimeout函数,而后在里面无限的递归。这里只须要注意一点,即每次发起滚动的间隔TRANSLATION_DELAY要大于过渡动画的时间,否则动画还没结束就会发动第二次滚动,这明显是不行的。

好了,还有一些边边角角的工做。加上按钮:

<div class="container">
    <div class="control-button previous"></div>
    <div class="control-button next"></div>
    <div class="pages">
        <div class="page" style="background: lightgreen;">1</div>
        <div class="page" style="background: lightblue;">2</div>
        <div class="page" style="background: lightgray;">3</div>
    </div>
</div>
复制代码

而后设置它们的点击事件:

document.querySelector('.previous').addEventListener('click', () => {
    if (lock) return;
    translatePage(parseInt(pagesStyle.marginLeft) + PAGE_WIDTH);
});
document.querySelector('.next').addEventListener('click', () => {
    if (lock) return;
    translatePage(parseInt(pagesStyle.marginLeft) - PAGE_WIDTH);
});
复制代码

这里的lock是用于确保过渡动画完成以前,不会发生第二次滚动,咱们在_translatePage中将锁开启:

function _translatePage(to, immediate) {
    lock = true;
    pages.className = 'pages' + (immediate ? ' immediate' : '');
    pages.style.marginLeft = to + 'px';
}
复制代码

而后在每次过渡动画结束后将锁解开:

pages.addEventListener('transitionend', () => lock = false, true);
复制代码

最后,在鼠标进入轮播图时,自动滚动会停滞,离开时轮播图自动启动。只须要利用mouseentermouseleave这两个事件就好了,它们只在鼠标进入或离开元素自己时被触发,而进入和离开其子元素时不会被触发。

container.addEventListener('mouseenter', () => clearTimeout(timer));
container.addEventListener('mouseleave', run);
复制代码

至此,因此功能已经实现,你能够在这里找到完整的代码。

相关文章
相关标签/搜索