經過上一篇文章的學習,咱們基本掌握了一個輪子的封裝和開發流程。那麼此次將帶你們開發一個更有難度的項目——輪播圖,但願能進一步加深你們對於面向對象插件開發的理解和認識。javascript
So, Let's begin!html
目前項目使用 ES5及UMD 規範封裝,因此在前端暫時只支持
<script>
標籤的引入方式,將來會逐步用 ES6 進行重構
演示地址: carousel carousel-mobile
Github: csdwheels
若是以爲好用就點個Star吧~(〃'▽'〃)
老規矩,在寫代碼以前,咱們須要對要開發的東西有個感性的認識,好比你能夠先在腦中大體過一遍最終的項目效果是如何的,而在這裏你能夠直接看上面的動態圖or項目頁面進行體驗。實際的開發階段以前,咱們更要對插件的邏輯思路有一個總體的分析,這樣在開發時纔會更有效率,而且能夠有效避免由於思路不清晰而致使的問題。前端
首先來看看Web輪播的效果及交互有哪些:vue
如上幾點,能夠說都是一個輪播圖必須實現的經典效果了。其餘效果先忽略,第六點對於新手來講明顯是最有難度的,事實上這個效果有個常見的名字——無縫輪播。「無縫」也能夠理解爲無限循環,其實就是可讓輪播朝着一個方向一直切換,而且自動在切換到頭尾圖片時循環。java
好比如今有五張圖片,咱們把它們編號爲:git
1 2 3 4 5
要實現上面的效果,你可能會想到在切換至頭尾時加個判斷,強制改變圖片位置,可是若是這麼作的話,當你最後一張圖切換回第一張圖時就會出現空白,所以還須要在頭尾分別添加一個尾部和頭部的元素做爲位置改變時的過渡:github
5 1 2 3 4 5 1
有了這兩張輔助圖,上面的效果就能順利實現了。到此,項目的基礎思路分析完畢,讓咱們進入編碼階段吧!web
正式開始以前,仍是須要先把項目的基本架構搭建起來:架構
(function(root, factory) { if (typeof define === "function" && define.amd) { define([], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(); } else { root.Carousel = factory(); } })(typeof self !== "undefined" ? self : this, function() { "use strict"; // ID-NAMES var ID = { CAROUSEL_WRAP: '#carouselWrap', CAROUSEL_DOTS: '#carouselDots', ARROW_LEFT: '#arrowLeft', ARROW_RIGHT: '#arrowRight' }; var CLASS = { CAROUSEL_WRAP: 'carousel-wrap', CAROUSEL_IMG: 'carousel-image', CAROUSEL_DOTS_WRAP: 'carousel-buttons-wrap', CAROUSEL_DOTS: 'carousel-buttons', CAROUSEL_DOT: 'carousel-button', CAROUSEL_DOT_ON: 'carousel-button on', CAROUSEL_ARROW_LEFT: 'carousel-arrow arrow-left', CAROUSEL_ARROW_RIGHT: 'carousel-arrow arrow-right' }; // Polyfills function addEvent(element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } } // 合併對象 function extend(o, n, override) { for (var p in n) { if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override)) o[p] = n[p]; } } // 輪播-構造函數 var Carousel = function (selector, userOptions) { var _this = this; // 合併配置 extend(this.carouselOptions, userOptions, true); // 獲取輪播元素 _this.carousel = document.querySelector(selector); // 初始化輪播列表 _this.carousel.appendChild(_this.getImgs()); // 獲取輪播列表 _this.carouselWrap = document.querySelector(ID.CAROUSEL_WRAP); // 每隔 50ms 檢測一次輪播是否加載完成 var checkInterval = 50; var checkTimer = setInterval(function () { // 檢測輪播是否加載完成 if (_this.isCarouselComplete()) { // 加載完成後清除定時器 clearInterval(checkTimer); // 初始化輪播 _this.initCarousel(); // 初始化圓點 _this.initDots(); // 初識化箭頭 _this.initArrows(); } }, checkInterval); }; // 輪播-原型對象 Carousel.prototype = { carouselOptions: { // 是否顯示輪播箭頭 showCarouselArrow: true, // 是否顯示輪播圓點 showCarouselDot: true, // 輪播自動播放間隔 carouselInterval: 3000, // 輪播動畫總時間 carouselAnimateTime: 150, // 輪播動畫間隔 carouselAnimateInterval: 10 }, isCarouselComplete: function () { // 檢測頁面圖片是否加載完成 var completeCount = 0; for (var i = 0; i < this.carouselWrap.children.length; i++) { if (this.carouselWrap.children[i].complete) { completeCount++; } } return completeCount === this.carouselWrap.children.length ? true : false; } }; return Carousel; });
addEvent()
和extend()
函數上篇已經介紹過了,構造函數中各類配置項也都是項目中要用到的,沒必要多說。這裏的重點是checkTimer
定時器,它的做用是每隔必定時間去檢查頁面上的圖片元素是否所有加載完畢,若是加載完畢再進行項目的初始化。
爲何須要這麼作呢?由於咱們的圖片元素是在JS中使用DOM動態加載的,因此可能會出現圖片還沒加載完成就執行了JS中的一些邏輯語句,致使不能經過DOM API正確獲取到圖片和對應元素屬性的現象。所以,在isCarouselComplete()
函數中咱們利用img
元素的complete
屬性,判斷當前頁面上的全部圖片是否加載完成,而後就能保證DOM屬性的正確獲取了。app
完成initCarousel()
函數:
initCarousel: function(selector, userOptions) { // 獲取輪播數量 this.carouselCount = this.carouselWrap.children.length; // 設置輪播 this.setCarousel(); // 初始化輪播序號 this.carouselIndex = 1; // 初始化定時器 this.carouselIntervalr = null; // 每次位移量 = 總偏移量 / 次數 this.carouselAnimateSpeed = this.carouselWidth / (this.carouselOptions.carouselAnimateTime / this.carouselOptions.carouselAnimateInterval); // 判斷是否處於輪播動畫狀態 this.isCarouselAnimate = false; // 判斷圓點是否點擊 this.isDotClick = false; // 綁定輪播圖事件 this.bindCarousel(); // 播放輪播 this.playCarousel(); }
經過this.carouselWidth / (this.carouselOptions.carouselAnimateTime / this.carouselOptions.carouselAnimateInterval)
這個公式,能夠計算出每次輪播動畫位移的偏移量,後面完成動畫函數時會用到。
在setCarousel()
裏進行輪播基本屬性的設置:
setCarousel: function () { // 複製首尾節點 var first = this.carouselWrap.children[0].cloneNode(true); var last = this.carouselWrap.children[this.carouselCount - 1].cloneNode(true); // 添加過渡元素 this.carouselWrap.insertBefore(last, this.carouselWrap.children[0]); this.carouselWrap.appendChild(first); // 設置輪播寬度 this.setWidth(this.carousel, this.carouselOptions.carouselWidth); // 設置輪播高度 this.setHeight(this.carousel, this.carouselOptions.carouselHeight); // 獲取輪播寬度 this.carouselWidth = this.getWidth(this.carousel); // 設置初始位置 this.setLeft(this.carouselWrap, -this.carouselWidth); // 設置輪播長度 this.setWidth(this.carouselWrap, this.carouselWidth * this.carouselWrap.children.length); }
添加首尾的過渡元素、設置高度寬度等。
而後是鼠標移入移出事件的綁定:
playCarousel: function () { var _this = this; this.carouselIntervalr = window.setInterval(function() { _this.nextCarousel(); }, this.carouselOptions.carouselInterval); }, bindCarousel: function () { var _this = this; // 鼠標移入移出事件 addEvent(this.carousel, 'mouseenter', function(e) { clearInterval(_this.carouselIntervalr); }); addEvent(this.carousel, 'mouseleave', function(e) { _this.playCarousel(); }); }
移入時中止輪播播放的定時器,移出後自動開始下一張的播放。
完成nextCarousel()
和prevCarousel()
函數:
prevCarousel: function () { if (!this.isCarouselAnimate) { // 改變輪播序號 this.carouselIndex--; if (this.carouselIndex < 1) { this.carouselIndex = this.carouselCount; } // 設置輪播位置 this.moveCarousel(this.isFirstCarousel(), this.carouselWidth); if (this.carouselOptions.showCarouselDot) { // 顯示當前圓點 this.setDot(); } } }, nextCarousel: function () { if (!this.isCarouselAnimate) { this.carouselIndex++; if (this.carouselIndex > this.carouselCount) { this.carouselIndex = 1; } this.moveCarousel(this.isLastCarousel(), -this.carouselWidth); if (this.carouselOptions.showCarouselDot) { // 顯示當前圓點 this.setDot(); } } }
功能是同樣的,改變輪播序號,而後進行輪播的移動。在moveCarousel()
中完成對過渡元素的處理:
moveCarousel: function (status, carouselWidth) { var left = 0; if (status) { left = -this.carouselIndex * this.carouselWidth; } else { left = this.getLeft(this.carouselWrap) + carouselWidth; } this.setLeft(this.carouselWrap, left); }
輪播相關屬性和事件的設置就完成了。
接下來是小圓點的事件綁定:
bindDots: function () { var _this = this; for (var i = 0, len = this.carouselDots.children.length; i < len; i++) { (function(i) { addEvent(_this.carouselDots.children[i], 'click', function (ev) { // 獲取點擊的圓點序號 _this.dotIndex = i + 1; if (!_this.isCarouselAnimate && _this.carouselIndex !== _this.dotIndex) { // 改變圓點點擊狀態 _this.isDotClick = true; // 改變圓點位置 _this.moveDot(); } }); })(i); } }, moveDot: function () { // 改變當前輪播序號 this.carouselIndex = this.dotIndex; // 設置輪播位置 this.setLeft(this.carouselWrap, -this.carouselIndex * this.carouselWidth); // 重設當前圓點樣式 this.setDot(); }, setDot: function () { for (var i = 0, len = this.carouselDots.children.length; i < len; i++) { this.carouselDots.children[i].setAttribute('class', CLASS.CAROUSEL_DOT); } this.carouselDots.children[this.carouselIndex - 1].setAttribute('class', CLASS.CAROUSEL_DOT_ON); }
功能很簡單,點擊圓點後,跳轉到對應序號的輪播圖,並重設小圓點樣式。
最後,還須要綁定箭頭事件:
bindArrows: function () { var _this = this; // 箭頭點擊事件 addEvent(this.arrowLeft, 'click', function(e) { _this.prevCarousel(); }); addEvent(this.arrowRight, 'click', function(e) { _this.nextCarousel(); }); }
這樣,一個沒有動畫的無縫輪播效果就完成了,見下圖:
上一節咱們分析後的思路基本是實現了,可是輪播切換時的動畫效果又該怎麼實現呢?
既然要實現動畫,那咱們先要找到產生動畫的源頭——即讓輪播發生切換的moveCarousel()
函數。所以,咱們須要先對它進行修改:
moveCarousel: function (target, speed) { var _this = this; _this.isCarouselAnimate = true; function animateCarousel () { if ((speed > 0 && _this.getLeft(_this.carouselWrap) < target) || (speed < 0 && _this.getLeft(_this.carouselWrap) > target)) { _this.setLeft(_this.carouselWrap, _this.getLeft(_this.carouselWrap) + speed); timer = window.setTimeout(animateCarousel, _this.carouselOptions.carouselAnimateInterval); } else { window.clearTimeout(timer); // 重置輪播狀態 _this.resetCarousel(target, speed); } } var timer = animateCarousel(); }
改造以後的moveCarousel()
函數接受兩個參數,target
表示要移動到的輪播的位置,speed
即爲咱們前面計算出的那個偏移量的值。而後經過animateCarousel()
函數進行setTimeout()
的遞歸調用,模擬出一種定時器的效果,當判斷未到達目標位置時繼續遞歸,若是到達後就重置輪播狀態:
// 不符合位移條件,把當前left值置爲目標值 this.setLeft(this.carouselWrap, target); //如當前在輔助圖上,就歸位到真的圖上 if (target > -this.carouselWidth ) { this.setLeft(this.carouselWrap, -this.carouselCount * this.carouselWidth); } if (target < (-this.carouselWidth * this.carouselCount)) { this.setLeft(this.carouselWrap, -this.carouselWidth); }
重置的過程和前面的實現是同樣的,再也不贅述。完成新的moveCarousel()
函數以後,還須要對prevCarousel()
及nextCarousel()
進行改造:
prevCarousel: function () { if (!this.isCarouselAnimate) { // 改變輪播序號 this.carouselIndex--; if (this.carouselIndex < 1) { this.carouselIndex = this.carouselCount; } // 設置輪播位置 this.moveCarousel(this.getLeft(this.carouselWrap) + this.carouselWidth, this.carouselAnimateSpeed); if (this.carouselOptions.showCarouselDot) { // 顯示當前圓點 this.setDot(); } } }, nextCarousel: function () { if (!this.isCarouselAnimate) { this.carouselIndex++; if (this.carouselIndex > this.carouselCount) { this.carouselIndex = 1; } this.moveCarousel(this.getLeft(this.carouselWrap) - this.carouselWidth, -this.carouselAnimateSpeed); if (this.carouselOptions.showCarouselDot) { // 顯示當前圓點 this.setDot(); } } },
其實就替換了一下moveCarousel()
調用的參數而已。完成這幾個函數的改造後,動畫效果初步實現了:
在頁面上進行實際測試的過程當中,咱們可能偶爾會發現有卡頓的狀況出現,這主要是由於用setTimeout()
遞歸後模擬動畫的時候產生的(直接用setInterval()
一樣會出現這種狀況),因此咱們須要用requestAnimationFrame
這個HTML5的新API進行動畫效率的優化,再次改造moveCarousel()
函數:
moveCarousel: function (target, speed) { var _this = this; _this.isCarouselAnimate = true; function animateCarousel () { if ((speed > 0 && _this.getLeft(_this.carouselWrap) < target) || (speed < 0 && _this.getLeft(_this.carouselWrap) > target)) { _this.setLeft(_this.carouselWrap, _this.getLeft(_this.carouselWrap) + speed); timer = window.requestAnimationFrame(animateCarousel); } else { window.cancelAnimationFrame(timer); // 重置輪播狀態 _this.resetCarousel(target, speed); } } var timer = window.requestAnimationFrame(animateCarousel); }
兩種方法的調用方式是相似的,可是在實際看起來,動畫卻流暢了很多,最重要的,它讓咱們動畫的效率獲得了很大提高。
到這裏,咱們的開發就結束了嗎?
用上面的方式實現完動畫後,當你點擊圓點時,輪播的切換是跳躍式的,並無達到咱們開頭gif中那種完成後的效果。要讓任意圓點點擊後的切換效果仍然像相鄰圖片同樣的切換,這裏還須要一種新的思路。
假如咱們當前在第一張圖片,這時候的序號爲1,而點擊的圓點對應圖片序號爲5的話,咱們能夠這麼處理:在序號1對應圖片節點的後面插入一個序號5對應的圖片節點,而後讓輪播切換到這張新增的圖片,切換完成後,當即改變圖片位置爲真正的序號5圖片,最後刪除新增的節點,過程以下:
第一步:插入一個新節點 5 1 5 2 3 4 5 1第二步:改變圖片位置,節點順序不變
第三步:刪除新節點,還原節點順序 5 1 2 3 4 5 1
用代碼實現出來就是這樣的:
moveDot: function () { // 改變輪播DOM,增長過渡效果 this.changeCarousel(); // 改變當前輪播序號 this.carouselIndex = this.dotIndex; // 重設當前圓點樣式 this.setDot(); }, changeCarousel: function () { // 保存當前節點位置 this.currentNode = this.carouselWrap.children[this.carouselIndex]; // 獲取目標節點位置 var targetNode = this.carouselWrap.children[this.dotIndex]; // 判斷點擊圓點與當前的相對位置 if (this.carouselIndex < this.dotIndex) { // 在當前元素右邊插入目標節點 var nextNode = this.currentNode.nextElementSibling; this.carouselWrap.insertBefore(targetNode.cloneNode(true), nextNode); this.moveCarousel(this.getLeft(this.carouselWrap) - this.carouselWidth, -this.carouselAnimateSpeed); } if (this.carouselIndex > this.dotIndex) { // 在當前元素左邊插入目標節點 this.carouselWrap.insertBefore(targetNode.cloneNode(true), this.currentNode); // 由於向左邊插入節點後,當前元素的位置被改變,致使畫面有抖動現象,這裏重置爲新的位置 this.setLeft(this.carouselWrap, -(this.carouselIndex + 1) * this.carouselWidth); this.moveCarousel(this.getLeft(this.carouselWrap) + this.carouselWidth, this.carouselAnimateSpeed); } }
須要注意的是,這裏要判斷點擊的圓點序號與當前序號的關係,也就是在當前序號的左邊仍是右邊,若是是左邊,還須要對位置進行重置。最後一步,完成新增節點的刪除函數resetMoveDot()
:
resetCarousel: function (target, speed) { // 判斷圓點是否點擊 if (this.isDotClick) { // 重置圓點點擊後的狀態 this.resetMoveDot(speed); } else { // 重置箭頭或者自動輪播後的狀態 this.resetMoveCarousel(target); } this.isDotClick = false; this.isCarouselAnimate = false; }, resetMoveDot: function (speed) { // 若是是圓點點擊觸發動畫,須要刪除新增的過分節點並將輪播位置重置到實際位置 this.setLeft(this.carouselWrap, -this.dotIndex * this.carouselWidth); // 判斷點擊圓點和當前圓點的相對位置 if (speed < 0) { this.carouselWrap.removeChild(this.currentNode.nextElementSibling); } else { this.carouselWrap.removeChild(this.currentNode.previousElementSibling); } },
查看一下效果:
大功告成!!
在Web版輪播的實現中,咱們對位置的控制是直接使用元素絕對定位後的left
值實現的,這種辦法雖然兼容性好,可是效率相對是比較低的。在移動端版本的實現中,咱們就能夠不用考慮這種兼容性的問題了,而能夠儘可能用更高效的方式實現動畫效果。
若是你們對CSS3有所瞭解,那想必必定知道transform
這個屬性。從字面上來說,它就是變形,改變的意思,而它的值大體包括旋轉rotate
、扭曲skew
、縮放scale
和移動translate
以及矩陣變形matrix
等幾種類型。咱們今天須要用到的就是translate
,經過使用它以及transition
等動畫屬性,能夠更高效簡潔的實現移動端圖片輪播的移動。
因爲基本思路與架構和Web版是差很少的,而H5版是基於Web版重寫的,因此這裏只說下須要改變的幾個地方。
既然是用新屬性來實現,那首先就要重寫setLeft()
和getLeft()
方法,這裏咱們直接替換爲兩個新方法:
setLeft: function (elem, value) { elem.style.left = value + 'px'; }, getLeft: function (elem) { return parseInt(elem.style.left); } setTransform: function(elem ,value) { elem.style.transform = "translate3d(" + value + "px, 0px, 0px)"; elem.style["-webkit-transform"] = "translate3d(" + value + "px, 0px, 0px)"; elem.style["-ms-transform"] = "translate3d(" + value + "px, 0px, 0px)"; }, getTransform: function() { var x = this.carouselWrap.style.transform || this.carouselWrap.style["-webkit-transform"] || this.carouselWrap.style["-ms-transform"]; x = x.substring(12); x = x.match(/(\S*)px/)[1]; return Number(x); }
新版的方法功能與老版徹底一直,只是實現所用到的方法不同了。接下來咱們須要一個transition
值的設置方法,經過這個動畫屬性,連requestAnimationFrame
的相關操做也不須要了:
setTransition: function(elem, value) { elem.style.transition = value + 'ms'; }
有了這三個方法,接下來就能夠重寫moveCarousel()
、resetCarousel()
和resetMoveCarousel()
方法了:
moveCarousel: function(target) { this.isCarouselAnimate = true; this.setTransition(this.carouselWrap, this.carouselOptions.carouselDuration); this.setTransform(this.carouselWrap, target); this.resetCarousel(target); }, resetCarousel: function(target) { var _this = this; window.setTimeout(function() { // 重置箭頭或者自動輪播後的狀態 _this.resetMoveCarousel(target); _this.isCarouselAnimate = false; }, _this.carouselOptions.carouselDuration); }, resetMoveCarousel: function(target) { this.setTransition(this.carouselWrap, 0); // 不符合位移條件,把當前left值置爲目標值 this.setTransform(this.carouselWrap, target); //如當前在輔助圖上,就歸位到真的圖上 if (target > -this.carouselWidth) { this.setTransform(this.carouselWrap, -this.carouselCount * this.carouselWidth); } if (target < -this.carouselWidth * this.carouselCount) { this.setTransform(this.carouselWrap, -this.carouselWidth); } }
之因此在每次setTransform()
改變位置以前都要從新設置transition
的值,是由於transition
會使每次位置的改變都帶上動畫效果,而咱們在代碼中作的過渡操做又不但願用戶直接看到,所以,重設它的值後才能和之前的實現效果保持一致。
在移動端上咱們一般習慣用手指直接觸摸屏幕來操做應用,因此Web端圓點和箭頭的交互方式這時候就顯得不那麼合適了,取而代之的,咱們能夠改寫成觸摸的交互方式,也就是touch
事件實現的效果:
bindCarousel: function() { var _this = this; // 鼠標移入移出事件 addEvent(this.carousel, "touchstart", function(e) { if (!_this.isCarouselAnimate) { clearInterval(_this.carouselIntervalr); _this.carouselTouch.startX = _this.getTransform(); _this.carouselTouch.start = e.changedTouches[e.changedTouches.length - 1].clientX; } }); addEvent(this.carousel, "touchmove", function(e) { if (!_this.isCarouselAnimate && _this.carouselTouch.start != -1) { clearInterval(_this.carouselIntervalr); _this.carouselTouch.move = e.changedTouches[e.changedTouches.length - 1].clientX - _this.carouselTouch.start; _this.setTransform(_this.carouselWrap, _this.carouselTouch.move + _this.carouselTouch.startX); } }); addEvent(this.carousel, "touchend", function(e) { if (!_this.isCarouselAnimate && _this.carouselTouch.start != -1) { clearInterval(_this.carouselIntervalr); _this.setTransform(_this.carouselWrap, _this.carouselTouch.move + _this.carouselTouch.startX); var x = _this.getTransform(); x += _this.carouselTouch.move > 0 ? _this.carouselWidth * _this.carouselTouch.offset : _this.carouselWidth * -_this.carouselTouch.offset; _this.carouselIndex = Math.round(x / _this.carouselWidth) * -1; _this.moveCarousel( _this.carouselIndex * -_this.carouselWidth ); if (_this.carouselIndex > _this.carouselCount) { _this.carouselIndex = 1; } if (_this.carouselIndex < 1) { _this.carouselIndex = _this.carouselCount; } _this.playCarousel(); } }); }
簡單來講,咱們把觸摸事件分爲三個過程——開始、移動、結束,而後在這三個過程當中,就能夠分別實現對應的邏輯與操做了:
經過這套邏輯,咱們模擬的移動設備的觸摸效果就能成功實現了:
文章自己只是對項目總體思路和重點部分的講解,一些細節點也不可能面面俱到,還請你們對照源碼自行理解學習~
最後我想說的是,相似輪播這樣的優秀插件其實已經有不少了,但這並不妨礙咱們寫一個本身的版本。由於只有本身寫一遍,並在腦中走一遍本身的思惟過程,而後在學習一些優秀的源碼及實現時纔不至於懵圈。
到止爲止,咱們第二個輪子的開發也算順利完成了,全部源碼已同步更新到github
,若是你們發現有bug或其餘問題,能夠回覆在項目的issue
中,我們後會有期!(挖坑不填,逃。。
已更新使用Webpack打包後的ES6版本,支持ES6模塊化引入方式。
import { Carousel } from 'csdwheels' import { CarouselMobile } from 'csdwheels'
具體的使用方法請參考README
To be continued...