從原理上理解,如何利用 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);
複製代碼

至此,因此功能已經實現,你能夠在這裏找到完整的代碼。

相關文章
相關標籤/搜索