Code a Fully Customized and Exquisitely Animated Carousel from Scratch

開發一個有 RWD、動畫、跳轉頁面、自動播放、手動播放、箭頭控制等的 Carousel

Posted by Young on 2023-03-18
Estimated Reading Time 10 Minutes
Words 2.4k In Total
Viewed Times

Goals

將舊的程式碼重構後,重新設計出一個自動抓取登錄中的活動並依照此活動的優先度、時間、狀態等等來排序,並且能夠自動播放、手動播放、鍵盤控制、滑鼠控制等的 RWD Carousel

由於此專案原本的 css 非常混亂,所以幾乎每個不同的 <div> 區塊都是參砸著 inline-css、Internal Style Sheet、External Style Sheet 且也可以看到很多當時工程師為了趕下班未經思考亂寫出來的、毫無可讀性、架構可言的 code。

後端程式碼涉及公司隱私的部分就不多做介紹,總之就是一樣去 DB 撈一個活動 table 的活動 ID 出來並跟另一個 table 做比對來判斷活動是否要顯示、活動種類、活動 URL…等。

所以此次工作目標拆分成小項目的話,可以分成以下幾個部分:

  1. 將傳統這種Tight Coupling(程式緊密耦合)的程式碼重構,將有關後端的程式碼模組化後再進行Back-end Data Injection
  2. 將原本參雜在 index 的 Carousel 程式區塊單獨取出來做一個 Component。
  3. 刪除原本的 CSS 並導入利用 TailwindCSS 達成 RWD,同時重新撰寫 CSS 與 JS 去控制元素顯示。
  4. 原始的程式碼是所有的圖片(除了第一張)預設隱藏的並在按鈕被點擊對應 id 時去執行 JS function 來顯示。CSS 的部分則是透過改變 display: nonedisplay: block 屬性來實現顯示對應 carousel img 的。
  5. 而現在要改成控制 active,來 addremove class 來達到顯示和隱藏的效果。
  6. 自動輪播功能、手動輪播功能、手動跳頁、鍵盤控制…等等。(不能互相衝突)

整個 Carousel 的 HTML Layout 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<div class="carousel-container relative">
<!-- Carousel items -->
<div class="carousel-inner">
<!-- some code here...-->
<div class="carousel-item">
<a href="<?= $activityUrl ?>">
<img
class="w-full object-cover cursor-pointer"
title="<?= $activityTitle ?>"
id="<?= $activityId ?>"
src='<?= $imagePath . $activityImage . "?t=" . $imgTimestamp ?>'
/>
</a>
</div>
<!-- some code here...-->
</div>

<!-- Carousel controls -->
<button
id="prevBtn"
class="absolute top-1/2 transform -translate-y-1/2 left-0 p-4 bg-gray-600 text-white opacity-50 rounded-lg hover:opacity-100 transition-opacity duration-300 ease-in-out cursor-pointer"
>

</button>
<button
id="nextBtn"
class="absolute top-1/2 transform -translate-y-1/2 right-0 p-4 bg-gray-600 text-white opacity-50 rounded-lg hover:opacity-100 transition-opacity duration-300 ease-in-out cursor-pointer"
>

</button>

<!-- Slider buttons -->
<div class="flex justify-center space-x-2 my-2">
<!-- some code here...-->
<button
class="sliderBtn w-3 h-3 bg-neutral-400 rounded-full focus:outline-none transition-colors duration-300 ease-in-out"
data-slide-to="<?= $index ?>"
></button>
<!-- some code here...-->
</div>
</div>

這邊設計重點有:

  • 容器化
  • 相對定位

通過使用相對定位,能更容易地控制 Carousel 中各個元素的位置和排列,使其達到我們所需的外觀和功能。

JS

基本樣式有了,但只顯示一張活動圖且不能互動也沒動畫效果顯然不是我們要的,

上面給每個容器都定義好了 class 屬性,接下來就可以透過 JS 來擷取這些元素並控制。

儘管專案內有引入 JQuery CDN,但未來還是可能要將 JQuery 淘汰所以還是用 vanilla JS 來撰寫程式,這樣好處是也較能理解 JS 以及底層的實際運作原理。

首先取得所有相關的元素並定義初始 index (預設 0,即第一張 image):

1
2
3
4
5
6
7
8
9
// Get carousel elements
const carouselItems = document.querySelectorAll(".carousel-item");
const carouselInner = document.querySelector(".carousel-inner");
const prevBtn = document.getElementById("prevBtn");
const nextBtn = document.getElementById("nextBtn");
const sliderBtns = document.querySelectorAll(".sliderBtn");

// Set initial slide index
let currentSlide = 0;

Creating X-axis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Function to show the current slide with transition
function showSlide(index) {
carouselItems.forEach((item, i) => {
item.classList.remove("active");
if (i === index) {
item.classList.add("active");
// Adjust the height of the carousel-inner to match the current slide
carouselInner.style.height = item.offsetHeight + "px";
}
});

// Update active slider button
sliderBtns.forEach((btn, i) => {
btn.classList.remove("active");
if (i === index) {
btn.classList.add("active");
}
});
}
  • 這裡利用 foreach 遍歷每個 carouselItems,首先會先每個 carouselItems.active 來確保只顯示當前要顯示的 carouselItems 的 image。
    • item:每個 carousel-item
    • i:每個 carousel-item 的索引。
  • 判斷如果(索引 === 傳進來的索引)的話,救代表此 carouselItems 為當前要顯示的 carouselItems,就加上 .active 來顯示。
  • carouselInner.style.height = item.offsetHeight + "px" 直接自動抓取目前 active 的 item 的高度並設定給 carousel-inner,這樣就能讓整個 Carousel 高度隨著圖片高度變化而變化,不會有空白的問題。

    這行非常重要,可確保幻燈片容器的高度與當前要顯示的幻燈片項的高度保持一致,避免在切換幻燈片時產生跳動或布局問題。

  • 獲取所有的 sliderBtns,同樣先移除所有的 .active,再判斷如果(索引 === 傳進來的索引)的話,就加上 .active 來顯示,以製作之後的 sliderBtnsactive 狀態。

Prev & Next & Slider Button Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Function to go to the previous slide
function prevSlide() {
currentSlide--;
if (currentSlide < 0) {
currentSlide = carouselItems.length - 1;
}
showSlide(currentSlide);
}

// Function to go to the next slide
function nextSlide() {
currentSlide++;
if (currentSlide >= carouselItems.length) {
currentSlide = 0;
}
showSlide(currentSlide);
}

// Event listeners for prev/next buttons
prevBtn.addEventListener("click", prevSlide);
nextBtn.addEventListener("click", nextSlide);
// Event listener for slider buttons
sliderBtns.forEach((btn, index) => {
btn.addEventListener("click", () => {
currentSlide = index;
showSlide(currentSlide);
});
});
// Show the initial slide
showSlide(currentSlide);

為了達到能無限循環播放效果,所以加了這兩個條件判斷:

  • 如果目前 currentSlide 小於 0 的話,就指派最後一張圖片的索引給 currentSlide
  • 反之如果 currentSlide 大於等於 carouselItems.length 的話,就將 currentSlide 設定為第一張圖片的索引。

而為了能讓使用者能點擊 sliderBtns 就跳到對應 index 的 carouselItems,這邊直接讓使用者點擊到的那個sliderBtnsindex觸發showSlide函式,並將currentSlide設定為對應index,這樣就能達到點擊sliderBtns就能跳到對應的carouselItems

Auto-play function

一般電商網站都一定會要求有自動播放的功能,才能吸引使用者的注意力。所以這裡也寫一個每隔 4 秒就自動播放下一張幻燈片的功能。

直接在開頭宣告一個 autoPlayTimer,並直接在 showSlide 函式後加上每隔 4 秒就觸發 nextSlide 函式的 autoPlayTimer

1
2
3
4
5
6
// 自動換頁計時器
let autoPlayTimer;
// Show the initial slide
showSlide(currentSlide);
// Start auto play
autoPlayTimer = setInterval(nextSlide, 4000); // Change the duration as needed (4 seconds in this example)

Reset Auto-play function

當使用者去手動透過 sliderBtns、prevBtn 還是 nextBtn 換頁時,都應該重置 autoPlayTimer,不然會造成使用者在手動換頁時,autoPlayTimer 也在自動播放,會造成連續換頁的不協調感。

定義一個 resetAutoPlayTimer 函式,用來重置 autoPlayTimer 的時間。

1
2
3
4
5
// Function to reset the auto-play timer
function resetAutoPlayTimer() {
clearInterval(autoPlayTimer);
autoPlayTimer = setInterval(nextSlide, 4000); // Change the duration as needed (4 seconds in this example)
}

並在 prevSlide、nextSlide、sliderBtns 函式中都加入 resetAutoPlayTimer(); 函式來重置 autoPlayTimer 的時間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Function to go to the previous slide
function prevSlide() {
currentSlide--;
if (currentSlide < 0) {
currentSlide = carouselItems.length - 1;
}
showSlide(currentSlide);
resetAutoPlayTimer();
}

// Function to go to the next slide
function nextSlide() {
currentSlide++;
if (currentSlide >= carouselItems.length) {
currentSlide = 0;
}
showSlide(currentSlide);
resetAutoPlayTimer();
}
// Event listener for slider buttons
sliderBtns.forEach((btn, index) => {
btn.addEventListener("click", () => {
showSlide(index);
resetAutoPlayTimer();
});
});

這樣當使用者去手動換頁、跳頁時就能不會再跟自動換頁互相干擾了。

CSS

這邊不用 tailwindcss 的類別去寫整個功能的 CSS 的原因有幾下幾點:

  • 複雜度和靈活性
  • 動畫需在特定情況下的進行控制
  • 程式碼的可讀性和可維護性

由於動畫效果常需要細粒度的控制和設置,包括定義關鍵幀、時間軸和各種動畫屬性。使用原生的 CSS 可提供更大的靈活性和自由度,以實現更複雜和精細的動畫效果。

在這使用 TailwindCSS 的實用類別和行內樣式語法可能會導致代碼變得冗長和難以閱讀,尤其是在處理多個動畫效果和交互時。TailwindCSS 主要就是拿來寫 RWD 以及一些客製化的元件樣式,對於這種還需要條件去判斷的動畫效果,還是用原生 CSS 較優勢。

以下是 CSS 的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.carousel-inner {
width: 100%;
position: relative;
/* 防止全部 img 同時水平展開 */
overflow: hidden;
}
.carousel-item {
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 0;
transform: translateX(100%);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.carousel-item.active {
opacity: 1;
transform: translateX(0);
}
.sliderBtn.active {
background-color: #2a2438;
transform: scale(1.2);
}

.sliderBtn {
animation: scale 1s ease-in-out;
}
  • 包覆 .carousel-inner 的容器要設定 position: relative,才能讓 .carousel-itemposition: absolute 能夠相對於 .carousel-inner 來定位。
  • carousel-itemposition: absolute 來定位才能將所有的 images 疊加在一起,只有當前是 active 的圖片是可見的,其他則會透過透明度和轉換移動到視圖外,創造出平滑過渡的效果。

關於 absoluterelative 的原理可以去看這篇 CSS relative? absolute?

簡而言之,就是當 .carousel-item設定 absolute後,就會會去往外層去尋找父元素有是否有position:relative | absolute | fixed | inherit 的元素,若是都沒有,就會以該網頁頁面(<body>)的左上角為定位點。

而這裡我將 carousel-inner 設定為 relative,是因為我希望 .carousel-item 的定位點是以 .carousel-inner 為定位點,就能防止每張 .carousel-item 出現位置不同導致圖片消失的情況發生。


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版