前段時間學習了淘寶首頁的靜態頁面,其中收穫較大的的就是這個迴圈播放的圖片輪播元件,本文就將相關製作經驗分享給大家。
先看看線上DEMO:原生JS迴圈播放圖片輪播元件 (支援IE8+,本文中的線上demo均未經過壓縮,可以直接在瀏覽器中除錯)
以及GitHub倉庫地址及完整程式碼:JuniorTour/simple-standard-js-carousel
一、思路講解:
1.先說基本的非迴圈無過渡圖片輪播:
這個思路還是很簡單的,通過觀察一些圖片輪播就可以發現,圖片輪播一般是以一個尺寸較小的父元素作為視窗,包裹住一組較長的長條狀的專案(item)子元素,再利用 overflow: hidden;
,將父元素作為“視窗”,只顯示出的專案子元素的一部分,並通過改變專案子元素的定位或translate3d屬性,實現多張圖片專案動態播放。
基本原理可以參考這個demo:圖片輪播基本原理演示
2.比較有意思的其實是迴圈的功能:
但是這樣簡單的輪播是不會迴圈播放的,也就是說當一輪圖片專案(item)播放到結尾;或者當在第一張圖(第一個專案)繼續向前時,就會超出內容子元素,出現空白部分,這一般不是我們想要的結果。
有多種思路可以實現迴圈播放,我觀察到淘寶網首頁的圖片輪播是這樣的思路:
複製開頭和結尾的專案,並分別放在開頭和結尾,當播放到開頭或結尾的專案,繼續播放,需要迴圈時,臨時取消transition屬性,並立即用定位跳轉至相應的真正的開頭或結尾之後,再恢復原來的transition,繼續正常滾動播放,從而利用視覺上的“欺騙”,實現帶有過渡效果的迴圈播放。
相應的原理可以參考這個demo:圖片輪播迴圈原理演示
二、HTML標記部分
核心理念是簡潔、語義化。這部分因為我學過bootstrap框架所以借鑑了bootstrap的HTML標記結構。
整體結構為:
外層的.carousel-wrapper
包裹著輪播的三個主要部分,分別是:
.carousel-item-wrapper
:專案內容部分(作為演示,本文中的demo使用a標籤代替了圖片,大家可以自行嘗試替換為圖片;同時新增了文字序號標記,以便於觀察理解,尤其要注意兩個複製的開頭和結尾專案copy-1和copy-5)。
.carousel-control-wrapper
:控制按鈕部分,即兩個用於控制左右移動的按鈕。
.carousel-index-wrapper
:索引按鈕部分,即圖片輪播中的那一排“小圓點”。為了便於用JS操控,我新增了id作為“鉤子”。而bootstrap在這裡用的是自定義的data屬性。
<div class="carousel-wrapper">
<div class="carousel-item-wrapper" style="left: -520px;">
<div class="carousel-item">
<a href="#">
<!--作為演示,用a標籤代替了圖片。-->
<!--<img src="img/carousel-img-5" alt="">-->
</a>
<div class="carousel-index-mark">
copy-5
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-1" alt="">-->
</a>
<div class="carousel-index-mark">
1
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-2" alt="">-->
</a>
<div class="carousel-index-mark">
2
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-3" alt="">-->
</a>
<div class="carousel-index-mark">
3
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-4" alt="">-->
</a>
<div class="carousel-index-mark">
4
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-5" alt="">-->
</a>
<div class="carousel-index-mark">
5
</div>
</div>
<div class="carousel-item">
<a href="#">
<!--<img src="img/carousel-img-1" alt="">-->
</a>
<div class="carousel-index-mark">
copy-1
</div>
</div>
</div>
<div class="carousel-control-wrapper">
<button id="prev">
<!--prev-->
<i><</i>
</button>
<button id="next">
<!--next-->
<i>></i>
</button>
</div>
<div class="carousel-index-wrapper">
<ul>
<li class="carousel-index-btn active-carousel-index-btn" id="carousel-to-1">carousel-index-1</li>
<li class="carousel-index-btn" id="carousel-to-2">carousel-index-2</li>
<li class="carousel-index-btn" id="carousel-to-3">carousel-index-3</li>
<li class="carousel-index-btn" id="carousel-to-4">carousel-index-4</li>
<li class="carousel-index-btn" id="carousel-to-5">carousel-index-5</li>
</ul>
</div>
</div>
三、CSS樣式部分
總的來說比較簡單,重要的地方我加上了註釋,有存疑的地方,歡迎和我交流。
/*reset*/
* {
border: none;
padding: 0;
margin: 0;
}
button {
outline: none;
}
li {
list-style: none;
}
.carousel-wrapper {
width:520px;
height:280px;
overflow: hidden; /*關鍵*/
position: relative;
margin: 100px auto;
}
.carousel-item-wrapper {
width:3640px;
height:280px;
position: absolute;
top: 0;
left: -520px;
transition: left .2s ease-in;
}
.carousel-item a {
display: block;
background-color: red;
width:520px;
height: 280px;
}
/*使用不同背景色的a替代圖片。*/
.carousel-item:nth-child(1) a {
background-color: rgb(129,194,214);
/*第五張圖片的複製*/
}
.carousel-item:nth-child(2) a {
background-color: rgb(129,146,214);
}
.carousel-item:nth-child(3) a {
background-color: rgb(217,179,230);
}
.carousel-item:nth-child(4) a {
background-color: rgb(220,247,161);
}
.carousel-item:nth-child(5) a {
background-color: rgb(131,252,216);
}
.carousel-item:nth-child(6) a {
background-color: rgb(129,194,214);
}
.carousel-item:nth-child(7) a {
background-color: rgb(129,146,214);
/*第一張圖片的複製*/
}
.carousel-item {
float: left;
}
.carousel-index-mark {
font-size:60px;
color: black;
position: absolute;
top: 0;
}
.carousel-control-wrapper {
transition: all .2s;
}
.carousel-wrapper:hover button {
display: block;
}
.carousel-control-wrapper button {
transition: all .2s linear;
display: none;
width:24px;
height:36px;
line-height:36px;
background-color: rgba(0,0,0,.3);
color: #fff;
position: absolute;
top: 50%;
cursor: pointer;
}
button#prev {
left:0;
}
button#next {
right:0;
}
button i {
font-size: 18px;
}
.carousel-index-wrapper {
width:65px;
height:13px;
overflow: hidden;
position: absolute;
bottom:15px;
left:50%;
margin-left: -33px;
}
.carousel-index-btn {
width:9px;
height:9px;
float: left;
margin:2px;
background-color: #b7b7b7;
border-radius: 50%;
text-indent: -999em;
/*這個-999em的文字對齊宣告有助於增強可訪問性。*/
cursor: pointer;
}
.active-carousel-index-btn {
background-color: #f44103;
}
四、JS部分
這一塊是主要部分,內容較多,因此我們逐步來實現各部分功能以便於理解。
0.功能和結構分析:
根據最開始的思路講解,我們把這個輪播的JavaScript功能大致分為以下4個部分:
1.左右滑動按鈕功能
2.索引按鈕跳轉功能
3.自動播放功能
4.迴圈播放功能。
我們來分別逐步實現。
1.實現左右滑動按鈕功能:
function addLoadEvent(func) {
var oldLoad = window.onload;
if (typeof oldLoad != `function`) {
window.onload = func;
} else {
window.onload = function () {
oldLoad();
func();
}
}
}
//給文件載入完成後的load事件繫結相應的處理函式:
addLoadEvent(preventDefaultAnchors);
addLoadEvent(carouselControl);
/*用一個物件把輪播元件的相關引數封裝起來,優點是靈活便於擴充套件升級;缺點是同時也增加了檔案的體積。*/
var carouselInfo = {
itemWidth: 520,
trueItemNum: 5,
itemNum: 7,
totalWidth: 7 * 520
};
//阻止a標籤預設的點選跳轉行為
function preventDefaultAnchors() {
var allAnchors = document.querySelectorAll(`a`);
for (var i = 0; i < allAnchors.length; i++) {
allAnchors[i].addEventListener(`click`, function (e) {
e.preventDefault();
}, false);
}
}
function carouselControl () {
var prev = document.querySelector("#prev");
var next = document.querySelector("#next");
var carouselWrapper = document.querySelector(".carousel-wrapper");
prev.onclick = function () {
slide(-1);
};
next.onclick = function () {
slide(1);
};
}
function slide(slideItemNum) {
var itemWrapper=document.querySelector(".carousel-item-wrapper");
var currentLeftOffset=(itemWrapper.style.left)?parseInt(itemWrapper.style.left): 0,
targetLeftOffset=currentLeftOffset-(slideItemNum*carouselInfo.itemWidth);
itemWrapper.style.left=targetLeftOffset+`px`;
}
第1步的demo:carousel-step-1
2.實現索引按鈕跳轉功能:
function carouselControl() {
var prev = document.querySelector("#prev");
var next = document.querySelector("#next");
var carouselWrapper = document.querySelector(".carousel-wrapper");
//新增索引按鈕的引用
var indexBtns = document.querySelectorAll(".carousel-index-btn");
//標記當前所在的圖片編號,用於配合控制.index-btn。
var currentItemNum = 1;
prev.onclick = function () {
//把滑動功能和切換索引按鈕功能裝入一個函式之中,以便於獲取當前索引:
currentItemNum=prevItem(currentItemNum);
};
next.onclick = function () {
//把滑動功能和切換索引按鈕功能裝入一個函式之中,以便於獲取當前索引:
currentItemNum=nextItem(currentItemNum);
};
for (var i = 0; i < indexBtns.length; i++) {
//利用立即呼叫函式,解決閉包的副作用,傳入相應的index值
(function (i) {
indexBtns[i].onclick = function () {
slideTo(i+1);
currentItemNum=i+1;
}
})(i);
}
}
function nextItem(currentItemNum) {
slide(1);
currentItemNum += 1;
if (currentItemNum == 6) currentItemNum = 1;
switchIndexBtn(currentItemNum);
return currentItemNum;
}
function prevItem(currentItemNum) {
slide(-1);
currentItemNum -= 1;
if (currentItemNum == 0) currentItemNum = 5;
switchIndexBtn(currentItemNum);
return currentItemNum;
}
//新增直接跳轉函式:
function slideTo(targetNum) {
var itemWrapper=document.querySelector(".carousel-item-wrapper");
itemWrapper.style.left=-(targetNum*carouselInfo.itemWidth)+`px`;
switchIndexBtn(targetNum);
}
function slide(slideItemNum) {
var itemWrapper = document.querySelector(".carousel-item-wrapper");
var currentLeftOffset = (itemWrapper.style.left) ? parseInt(itemWrapper.style.left) : 0,
targetLeftOffset = currentLeftOffset - (slideItemNum * carouselInfo.itemWidth);
itemWrapper.style.left = targetLeftOffset + `px`;
}
function switchIndexBtn(targetNum) {
//切換當前的索引按鈕
//刪除過去啟用的.active-carousel-index-btn
var activeBtn=document.querySelector(".active-carousel-index-btn");
activeBtn.className=activeBtn.className.replace(" active-carousel-index-btn","");
//新增新的啟用索引按鈕
var targetBtn=document.querySelectorAll(".carousel-index-btn")[targetNum-1];
targetBtn.className+=" active-carousel-index-btn";
}
第2步的demo:carousel-step-2
3.實現自動播放功能:
function carouselControl() {
//省略前面重複的程式碼......
for (var i = 0; i < indexBtns.length; i++) {
//利用立即呼叫函式,解決閉包的副作用,傳入相應的index值
(function (i) {
indexBtns[i].onclick = function () {
slideTo(i+1);
currentItemNum=i+1;
}
})(i);
}
//新增定時器
var scrollTimer;
function play() {
scrollTimer=setInterval(function () {
currentItemNum=nextItem(currentItemNum);
},2000);
}
play();
function stop() {
clearInterval(scrollTimer);
}
//繫結事件
carouselWrapper.addEventListener(`mouseover`,stop);
carouselWrapper.addEventListener(`mouseout`,play,false);
/*DOM二級的addEventListener相對於on+sth的優點是:
* 1.addEventListener可以先後新增多個事件,同時這些事件還不會相互覆蓋。
* 2.addEventListener可以控制事件觸發階段,通過第三個可選的useCapture引數選擇冒泡還是捕獲。
* 3.addEventListener對任何DOM元素都有效,而不僅僅是HTML元素。*/
}
第3步的demo:carousel-step-3
4.關鍵點:實現迴圈播放功能:
function slide(slideItemNum) {
var itemWrapper = document.querySelector(".carousel-item-wrapper");
var currentLeftOffset = (itemWrapper.style.left) ? parseInt(itemWrapper.style.left) : 0,
targetLeftOffset = currentLeftOffset - (slideItemNum * carouselInfo.itemWidth);
/*不在這裡跳轉了。先處理偏移值,實現迴圈,再跳轉。*/
//itemWrapper.style.left = targetLeftOffset + `px`;
switch (true) {
/*switch 的語法是:當case之中的表示式等於switch (val)的val時,執行後面的statement(語句)。*/
case (targetLeftOffset>0):
itemWrapper.style.transition="none";
itemWrapper.style.left=-carouselInfo.trueItemNum*carouselInfo.itemWidth+`px`;
/*此處即相當於:itemWrapper.style.left=`-2600px`;*/
targetLeftOffset=-(carouselInfo.trueItemNum-1)*carouselInfo.itemWidth;
//相當於:targetLeftOffset=-2080;
break;
case (targetLeftOffset<-(carouselInfo.totalWidth-carouselInfo.itemWidth)):
//此處即相當於:targetLeftOffset<-3120
itemWrapper.style.transition="none";
itemWrapper.style.left=-carouselInfo.itemWidth+`px`;
//相當於:itemWrapper.style.left=`-520px`;
targetLeftOffset=-carouselInfo.itemWidth*2;
//相當於:targetLeftOffset=-1040;
break;
}
/*這裡我使用了setTimeout(fn,0)的hack
* 參考bootstrap的carousel.js原始碼,似乎也利用了setTimeout(fn,0)這一hack。
*
* stackoverflow上有對這一hack的討論和解釋:
* http://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful
* 根據第二個回答,我個人的理解是:setTimeout(fn,0)相當於非同步執行內部的程式碼fn,
* 具體到這個輪播,就是在上一輪非過渡定位的頁面渲染工作(switch語句內部的case)結束之後,再執行setTimeout內部的過渡位移工作。
* 從而避免了,非過渡的定位還未結束,就恢復了過渡屬性,使得這一次非過渡的定位也帶有過渡效果。
**/
//各位可以試一試,把setTimeout內部的程式碼放在外部,“迴圈”時會有什麼樣的錯誤效果。
//itemWrapper.style.transition="left .2s ease-in";
//itemWrapper.style.left=targetLeftOffset+`px`;
setTimeout(function () {
itemWrapper.style.transition="left .2s ease-in";
itemWrapper.style.left=targetLeftOffset+`px`;
},20);
}
第4步的demo:carousel-step-4
至此,就完成了一個完整的迴圈播放圖片輪播,欣賞一下自己的傑作吧~~~ヾ(✿゚▽゚)ノ
五、原始碼及示例:
1.GitHub倉庫地址及完整程式碼:JuniorTour/simple-standard-js-carousel
2.線上demo:原生JS迴圈播放圖片輪播元件
很慚愧,只做了一點簡單的工作。如果覺得本文不錯的話,歡迎給我的GitHub點贊!