嗨~ 這裏是芝麻,今天咱們一塊來作一個「滑塊插件」。那麼啥是滑塊插件呢?滑塊插件能幹嗎呢?請看下圖:
javascript
是否是有點印象了,沒錯,他的最基本的用法就是左右滑動,插件使用者只須要寫幾行簡單的html和js便可實現一個簡單滑動效果,不過你徹底能夠組合各類元素來適應不一樣的場景。css
固然插件我已經寫好了,咱先看下這個插件是怎麼來用的,對插件有一個大概瞭解,一會寫起來不至於太懵逼。。。html
插件地址:github.com/laravuel/sw… demo目錄有演示和用法,不過插件我用了webpack和babel轉碼,能夠不用管,直接看src/swiper.js便可。vue
<!-- demo.html -->
<!-- swiper名稱能夠自定義的啦 -->
<div id="swiper">
<!-- swiper-item名稱也能夠自定義啦,至關於一個滑塊 -->
<div class="swiper-item">
<img src="./images/1.jpg" />
</div>
<div class="swiper-item">
<img src="./images/2.jpg" />
</div>
<div class="swiper-item">
<img src="./images/3.jpg" />
</div>
</div>
<script src="../dist/swiper.js"></script>
<script> new Swiper({ swiper: '#swiper', // swiper節點名稱 item: '.swiper-item', // swiper內部滑塊的節點名稱 autoplay: false, // 是否自動滑動 duration: 3000, // 自動滑動間隔時間 change(index) { // 每滑動一個滑塊,插件就會觸發change函數,index表示當前的滑塊下標 console.log(index); } }); </script>
複製代碼
就是這麼簡單,插件自己只是一個類,你只須要new一個對象出來,而後傳遞一些參數就ok了。並且,插件還提供了一個change方法,讓使用者能夠在外部控制滑塊的滑動!java
const swiper = new Swiper({...});
swiper.change(2); // 滑動到第三個滑塊
複製代碼
那麼接下來,就是咱們的教程時間了,我也不肯定你能不能硬着頭皮看完,不過我敢確定,若是你可以親手把插件寫出來,你確定會開心的飛起!!!jquery
因爲本次教程內容比較多,因此我分上下兩部分來說,第一部分主要講解原理,第二部分開始着手編寫插件。因此,感興趣的小夥伴能夠加個關注先。
webpack
俗話說,一上來就貼代碼純屬耍流氓~ 咱們要清楚本身想實現哪些功能,懶得思考的童鞋能夠結合我上面的動圖來分析:git
- 滑塊能夠左右滑動(支持移動端和pc端)
- 滑塊塊內部能夠寫任何元素
- 滑動到第一個和最後一個滑塊時會有一個限制,防止越界
- 可以自動播放
咱們所能看到的大概就這些,接下來咱們會對這些功能一一進行拆解和分析。
github
上面簡單梳理了一些功能,其實能夠再擴展出如下幾個問題:web
別急,一個個來
咱們先來看一張圖:
這就是一個滑塊的最基本的結構圖,有三個部分組成:
- 視圖 咱們的內容展現區域,至關於最外層的一個展現層
- 容器 容器的寬度是無限長的,容納咱們全部須要切換的內容,滑塊的左右滑動,實質上是容器的左右移動(left),而每一個滑塊相對於容器實際上是靜止的
- 滑塊 一個個的內容
那麼根據這個結構,能夠用以下html代碼來表示:
<!-- 視圖層 -->
<div class="swiper">
<!-- 容器 -->
<div class="swiper-container">
<!-- 滑塊 -->
<div class="swiper-item" style="background: #000">1</div>
<div class="swiper-item" style="background: #4269eb">2</div>
<div class="swiper-item" style="background: #247902">3</div>
</div>
</div>
複製代碼
而後再配上css樣式:
.swiper {
position: relative;
width: 300px;
/* 下面是爲了讓你們看的更清楚,加的修飾 */
padding: 30px 0;
margin: 0 auto;
background: #FFB973;
}
.swiper .swiper-container {
position: relative;
/* 爲啥要設置-300px呢,由於我想讓他默認在第二個滑塊的位置,一會會給你們演示 */
left: -300px;
/* 讓容器儘量的寬,這樣才能容納更多的滑塊 */
width: 10000%;
/* 讓內部滑塊能夠排成一行 */
display: flex;
/* 下面是爲了讓你們看的更清楚,加的修飾 */
background: red;
padding: 15px 0;
}
.swiper .swiper-container .swiper-item {
/* 寬度設置1%會按照外層視圖的寬度來鋪滿 */
width: 1%;
height: 300px;
background: #eee;
/* 下面是爲了讓你們看的更清楚,加的修飾 */
text-align: center;
font-size: 40px;
color: #fff;
}
複製代碼
你就會看到這麼個效果:
固然,你能夠把我加的修飾css樣式都給去掉,而後再試試。
若是你可以理解上面的html結構的話,那咱們就能夠進行滑動的講解了。(若是還不理解的話,那就繼續往下看吧~或許會忽然恍然大悟!)
上面咱們提到了「滑塊容器」這個概念,滑塊的左右移動就是他來負責的
.swiper .swiper-container {
position: relative;
left: -300px;
}
複製代碼
由於滑塊的寬度是和視圖的寬度同樣的,因此咱們這裏滑塊的寬度是300px,那麼咱們把容器的left設置爲-300px,就至關於向左移動了一個滑塊的寬度,設置爲-600px就表示向左移動了兩個滑塊的寬度,懂了吧,若是你想移動到某個滑塊,那麼只須要知道這個滑塊的順序(從0開始),而後乘以滑塊寬度的相反數就好了,好比要移動到第三個滑塊,他的順序是2,那麼就是2 * -300 = -600
看下面動圖演示:
可是好像並無出現滑動的動畫效果耶,廢話,還沒寫呢,有些童鞋可能喜歡用jquery
,習慣了他的animate動畫方法,說實話其實我不太喜歡,由於我以爲css自帶的動畫徹底能夠解決大部分需求,並且當你之後用了vue這種mvvm框架,你會發現jquery這種動畫方式很不實用!
扯遠了,不過今天咱們不用css的animation
屬性,咱們用另一個屬性transition
就能夠知足,看名字你也能猜到,就是一個過渡屬性,詳細的用法請參考:developer.mozilla.org/zh-CN/docs/…
咱們把容器加上transition
屬性試試看哈:
.swiper .swiper-container {
/* 省略... */
transition: left 0.2s ease-in-out;
}
複製代碼
transition: left 0.2s ease-in-out;
是表示:
若是元素的left值變動,那麼會有一個0.2s的過渡動畫(補間動畫)
到這裏,我以爲你應該能理解了吧,每一個滑塊swiper-item
的左右滑動,並非滑塊自己在移動,而是他的父元素swiper-container
容器在左右移動(left值變化),而後咱們用transition
屬性來讓這個變化過程出現一個過渡動畫效果!
上面咱們扯了一堆html和css,接下來咱們說點js吧。 「如何來觸發滑動?」,咱們先不考慮手機端,就按照pc網頁來,那麼觸發操做就是在容器上按住鼠標向左/右拖動,而後鬆開鼠標後,滑塊就會向左/右滑動。
整個流程都跟鼠標事件掛鉤:
mousedown
鼠標按下事件mousemove
鼠標移動事件mouseup
鼠標擡起事件利用好這3個事件,咱們就能夠來實現鼠標控制滑塊移動了!!咱們先來實現摁住鼠標向左、向右拖動滑塊。 既然咱們的容器swiper-container
是負責左右移動的,那麼咱們就來監聽他的鼠標事件吧,首先用querySelector
獲取視圖和容器兩個元素節點:
// 首先獲取視圖層元素
const swiperEl = document.querySelector('.swiper');
// 在視圖層裏邊查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container');
複製代碼
獲取到容器元素後,就能夠用他的addEventListener
來監聽事件了:
containerEl.addEventListener('mousedown', (event) => {
console.log('鼠標按下了');
});
containerEl.addEventListener('mousemove', (event) => {
console.log('鼠標移動了');
});
containerEl.addEventListener('mouseup', (event) => {
console.log('鼠標擡起了');
});
複製代碼
看下動圖操做:
雖然咱們能夠成功的監聽到鼠標的操做事件,可是好像有點問題,咱們指望的結果是,只有當鼠標按下後纔會觸發鼠標移動操做,可是如今看來並無,因此能夠考慮加一個狀態來控制。
let state = 0; // 鼠標默認狀態
containerEl.addEventListener('mousedown', (event) => {
state = 1; // 設置爲1表示按下了鼠標
console.log('鼠標按下了');
});
containerEl.addEventListener('mousemove', (event) => {
if (state != 1) return; // 只有當state == 1時候才容許執行該事件
console.log('鼠標移動了');
});
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢復默認狀態
console.log('鼠標擡起了');
});
複製代碼
這樣就好多了!!!
那麼鼠標事件有了,接下來要讓容器跟着鼠標左右動才行。
咱們要知道,瀏覽器對於鼠標的任何操做,都會有一個座標參數(pageX和pageY),因此,咱們能夠根據鼠標移動時候的座標參數來計算容器的left值,你能夠想象一下,當你摁下鼠標而後左右移動,鼠標每次移動相對於上次都會產生一個距離,咱們是否是能夠把容器的left值加上或者減去這個距離,從而達到一個拖動效果呢?記得前面咱們回調函數裏邊的event
參數了嗎,他就是鼠標當前操做的相關屬性,而咱們目前只須要用到pageX屬性
transition
這個屬性給註釋掉,後面會解釋爲何?
.swiper .swiper-container {
/* 省略... */
/* transition: left 0.2s ease-in-out; */
}
複製代碼
每一步的操做,都在註釋裏邊詳細標註:
// 首先獲取視圖層元素
const swiperEl = document.querySelector('.swiper');
// 在視圖層裏邊查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container');
let state = 0; // 鼠標默認狀態
let oldEvent = null; // 用來記錄鼠標上次的位置
// 獲取容器的初始left值
let left = containerEl.offsetLeft;
containerEl.addEventListener('mousedown', (event) => {
state = 1; // 設置爲1表示按下了鼠標
oldEvent = event; // 當鼠標按下時候記錄初始位置
console.log('鼠標按下了');
});
containerEl.addEventListener('mousemove', (event) => {
if (state != 1) return; // 只有當state == 1時候才容許執行該事件
// 用當前鼠標的位置來和上次鼠標的位置做比較
// 若是當前鼠標的pageX小於上次鼠標的pageX,那就表示鼠標在向左拖動,就須要把容器left值減去鼠標移動的距離
if (event.pageX < oldEvent.pageX) {
left -= oldEvent.pageX - event.pageX;
}
else {
left += event.pageX - oldEvent.pageX;
}
// 完事以後記得把當前鼠標的位置賦值給oldEvent
oldEvent = event;
// 最後再把left賦值給容器
containerEl.style.left = left + 'px';
console.log('鼠標移動了');
});
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢復默認狀態
console.log('鼠標擡起了');
});
複製代碼
運行看效果:
但是,但是,你這鼠標鬆開後,也沒滑動到對應位置啊,額,額,前面咱們不是講了嘛,滑塊順序、滑塊寬度還記得麼?0 - 滑塊順序 * 滑塊寬度就會移動到這個滑塊,還記得不?
咱們用index
來記錄當前滑塊的順序
let index = 0; // 記錄當前滑塊的順序(從0開始)
複製代碼
用itemWidth
來存儲滑塊的寬度
// 獲取到全部的滑塊元素
const itemEls = containerEl.querySelectorAll('.swiper-item');
// 獲取到滑塊的寬度
const itemWidth = itemEls[0].offsetWidth;
複製代碼
把咱們的left
變量改一下,以前left
變量是直接獲取容器元素的left值,如今咱們要根據index
來計算
// let left = containerEl.offsetLeft;
// 存儲容器的left,這裏咱們根據index來計算初始容器的left值
let left = 0 - itemWidth * index;
// 設置容器的初始位置
containerEl.style.left = left + 'px';
複製代碼
這樣咱們只須要修改index
變量的值,那麼容器初始位置就會發生變化。
而後,咱們在鼠標按下的時候,記錄下座標位置,在鼠標擡起的時候拿當前鼠標的位置和按下的位置做比較,來判斷用戶是向左劃的,仍是向右劃的!
let startEvent = null; // 用來記錄鼠標按下時候的位置(最初位置)
containerEl.addEventListener('mousedown', (event) => {
state = 1; // 設置爲1表示按下了鼠標
startEvent = oldEvent = event; // 當鼠標按下時候記錄初始位置
console.log('鼠標按下了');
});
複製代碼
那麼鼠標擡起的時候,只須要和startEvent.pageX
作比較,就能夠判斷出左滑仍是右滑,左滑咱們讓index + 1
,右滑就讓index - 1
,最終咱們經過index
再來計算left
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢復默認狀態
// 鼠標擡起時候,和按下的座標做比對,用來判斷是向左滑動仍是向右滑動
// 向左滑動那麼就是要顯示下一個滑塊,因此index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
}
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠標擡起了');
});
複製代碼
transition
麼,爲何要註釋掉呢,由於只有在鼠標擡起的那一刻才須要滑動動畫,左右拖動是根據鼠標位移距離來計算left,數值很小,徹底不須要銜接動畫,因此,咱們先把註釋掉那個
transition
代碼單獨提取出來放到一個和
swiper-container
同級的
.move
類裏邊,當鼠標擡起的時候,咱們把
swiper-container
追加一個
move
類就行。
.swiper .swiper-container.move {
transition: left 0.2s ease-in-out;
}
複製代碼
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢復默認狀態
// 鼠標擡起時候,和按下的座標做比對,用來判斷是向左滑動仍是向右滑動
// 向左滑動那麼就是要顯示下一個滑塊,因此index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
}
// 追加一個move樣式
containerEl.className += ' move';
// 當過分動畫結束後,必定要把這個類給移除掉
containerEl.addEventListener('transitionend', () => {
// 正則替換 \s+ 表示一個或多個空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠標擡起了');
});
複製代碼
注意觀察swiper-container
的dom節點:
index
就行,看代碼:
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢復默認狀態
// 鼠標擡起時候,和按下的座標做比對,用來判斷是向左滑動仍是向右滑動
// 向左滑動那麼就是要顯示下一個滑塊,因此index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
}
// 防止滑塊越界
// 若是當前滑塊是第一個,向右滑動後,回到第一個滑塊
// 若是是最後一個,向左滑動後,回到最後一個滑塊
if (index < 0) {
index = 0;
}
else if (index > itemEls.length - 1) {
index = itemEls.length - 1;
}
// 追加一個move樣式
containerEl.className += ' move';
// 當過分動畫結束後,必定要把這個類給移除掉
containerEl.addEventListener('transitionend', () => {
// 正則替換 \s+ 表示一個或多個空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠標擡起了');
});
複製代碼
回到咱們的功能列表,咱們來看下第四條「自動播放」,第一個想到的是setInterval
setInterval(() => {
// 這個回調會每隔2秒執行一次
}, 2000);
複製代碼
因此,咱們只須要在這個回調函數裏邊寫上讓滑塊滑動的代碼不就好了? 咱們是用index
變量來控制當前滑塊的,那麼每隔2秒讓index
加1,最後再根據index
計算出left
的值,不就能夠了?
setInterval(() => {
// 默認向左滑動
index ++;
// 若是滑動到最後一個滑塊,則回到第一個滑塊
if (index > itemEls.length - 1) {
index = 0;
}
// 下面的代碼跟咱們鼠標擡起的事件的代碼同樣的,要不要考慮簡單的封裝一下?
// 追加一個move樣式
containerEl.className += ' move';
// 當過分動畫結束後,必定要把這個類給移除掉
containerEl.addEventListener('transitionend', () => {
// 正則替換 \s+ 表示一個或多個空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
}, 2000);
複製代碼
不過他總是這麼自動播放也不是個事,有時候我想看看內容,還沒看完呢,就自動划走了,因此,咱們能夠當鼠標放在容器上的時候,中止播放,鼠標移開後又恢復自動播放
mouseover
鼠標移動到某個元素上mouseout
鼠標在某個元素上移開
咱們仍是在容器上監聽這兩個事件,並用一個狀態autoplay
來控制播放:
// 自動播放狀態
let autoplay = true;
setInterval(() => {
if (!autoplay) return;
// 默認向左滑動
index ++;
// 若是滑動到最後一個滑塊,則回到第一個滑塊
if (index > itemEls.length - 1) {
index = 0;
}
// 追加一個move樣式
containerEl.className += ' move';
// 當過分動畫結束後,必定要把這個類給移除掉
containerEl.addEventListener('transitionend', () => {
// 正則替換 \s+ 表示一個或多個空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
}, 2000);
containerEl.addEventListener('mouseover', () => {
// 鼠標移動到容器上,中止播放
autoplay = false;
});
containerEl.addEventListener('mouseout', () => {
// 鼠標從容器上移開,恢復播放
autoplay = true;
});
複製代碼
clearInterval
函數等。
至此,咱們的原理都講的差很少了,有遺漏的地方,還望指出,那麼在第二部分,我會和你們一塊來把寫的雜七雜八的代碼作一個封裝,讓咱們的代碼插件化,適應更多的場景。