移動端tab滑動和上下拉刷新加載
查看demo(請在移動端模式下查看)
查看代碼
開發該插件的初衷是,在做一個項目時發現現在實現移動端tab滑動的插件大多基於swiper,swiper的功能太強大而我只要一個小小的tab滑動功能,就要引入200+k的js這未免太過浪費。而且swiper是沒有下拉刷新功能的,要用swiper實現下拉刷新還得改造一番。在實現功能的同時產生了不少bug。要是在引入一個下拉刷新的插件又難免多了幾十kb的js。而且這些插件對dom結構又是有一定要求的,一不小心就有bug。修復bug的時間都可以在擼一個插件出來了。
這次開發的這個插件只依賴手勢庫touch.js。使用原生實現功能。大小只有6kb。兼容性也算不錯。
其實對touch.js的依賴並不嚴重,只是用了其兩個手勢事件,花點時間完全可以自己實現的。
插件我只是粗略的測試了一番,若有什么bug請大家提出。有寫的不清楚的也請提出。覺得不錯的可以給我一個星星
- 該插件基於百度手勢庫touch.js該改手勢庫的大小也只有13k不到。官方文檔鏈接是找不到了所以引用別人寫的吧:API文檔
總結一下這次開發插件的原理和所遇到的坑吧
實現的話主要分為:
- 確定好容器結構
- 捕獲滑動的事件(使用touch.js獲取滑動的方向、滑動的距離和滑動的速度)
- 實現滑動的效果(這里使用的是transform來實現滑動, transtion來實現動畫)
- 確定臨界點(根據滑動不同的距離判斷是否切換頁數,還有根據滑動的速度確定是否切換頁數)
- 暴露監聽事件(產生不同狀態的回調)
坑:
主要體現在微信和ios瀏覽器對下拉時會有彈簧效果:
這個是瀏覽器的默認效果,是可以通過“e.preventDefault()”取消默認效果的。不過這就會產生容器不能滾動了。
所以就不能直接e.preventDefault()取消默認效果了。只能在特定的條件下才能取消默認事件。那條件是什么呢?
第一個條件就是滑動方向是向下&&是在容器頂部時候
第二個條件就是滑動方向向下&&在容器底部
在這里touch.js可以輕易的獲取滑動的方向,滾動條所在的位置也很容易算出。我已開始也以為很簡單的,結果卻發現touch.js獲取滾動方向是有一定延時的,這就造成第一時間捕獲的位置是上一次的,所以出現偶爾可以偶爾不可,有時干脆滾動不了。所以使用touch.js獲取方向的方式是不可取的。
只能自己采集觸摸屏幕時的坐標,在對比滑動時的坐標取得方向。ok這個bug就這樣輕松解決了。這都是在微信上運行的結構,后來拉到uc的時候竟然發現uc連左右滑動都有默認效果(喪盡天良)。
這就只能用老辦法解決了,增加兩組條件,左右滑動。根據采集的初始點,對比滑動過程的坐標,判斷上下滾動還是左右滑動。在取消默認效果。
API:
dom結構:
<div id="box"> <!-- 主容器 -->
<div class="pullDownHtml"> <!-- 下拉刷新的顯示內容 -->
<div class="pullDownshow1">下拉刷新</div>
<div class="pullDownshow2">正在刷新</div>
</div>
<div class="pullUpHtml"> <!-- 上拉加載的顯示內容 -->
<div class="pullUpHtmlshow1">上拉加載</div>
<div class="pullUpHtmlshow2">正在加載</div>
</div>
<div class="box">
<div class="tab-container">
<div class="s-pull">
// 頁面一內容
</div>
</div>
<div class="tab-container">
<div class="s-pull">
// 頁面二內容
</div>
</div>
<div class="tab-container">
<div class="s-pull">
// 頁面三內容
</div>
</div>
</div>
</div>
1、初始化
var swiper = new TabSwiper(ele, options)
// ele:容器
// options: 參數(Object)
2、options參數
{
speed: 300, // 動畫速度
threshold: 100, // 上下拉觸發的閥值(px)
xThreshold: 0.3, // 左右滑動觸發的閥值(0~1)默認為:‘0.25’
closeInertia: false, // 是否關閉慣性滑動, 默認開啟
isPullDown: true, // 是否開啟下拉刷新
isPullUp: true, // 是否開啟上拉加載
defaultPage: 0, // 默認顯示的頁數
initCb: function(){}, // 初始化回調
onEnd: function(page){}, // 切換頁數時回調(返回當前頁數)
onRefreshStart: function(page){}, // 觸發下拉刷新時回調(返回當前頁數)
onLoadStart: function(page){}, // 觸發上拉加載時回調(返回當前頁數)
onTouchmove: function(page, e){} // 正在頁面上滑動回調(返回當前頁數和滑動信息。可通過滑動的信息得到當前滑動的方向速度滑動的距離,進行功能擴展)
}
3、pullEnd(cb)方法:
swiper.pullEnd(function (page) { // 返回當前頁數
console.log(page)
})
4、changePage(page)方法:
swiper.changePage(page) // 切換頁面page目標頁面從0開始
5、nowIndex屬性:
var nowIndex = swiper.nowIndex // 獲取當前所在頁數(只讀)
下面是代碼(基於es6)
若要查看es5的版本請移步(查看代碼)
;(function (window, document) {
// 更改transform
function changeTransform (ele, left, top) {
ele.style.transform = `translate(${left}px, ${top}px)`
ele.style.WebkitTransform = `translate(${left}px, ${top}px)`
}
class TabSwiper {
get nowIndex () {
return this._nowIndex
}
set nowIndex (val) {
if (val === this._nowIndex) return
this._nowIndex = val
this.options.onEnd && this.options.onEnd(val)
}
constructor (ele, options) {
this._nowIndex = 0
this.ele = ele
this.width = ele.clientWidth // 容器寬度
this.height = ele.clientHeight // 容器高度
this.totalWidth = 0 // 總寬度
this.box = ele.querySelector('.box')
this.containers = ele.querySelectorAll('.tab-container') // 容器
this.direction = ''
this.scrollTop = 0
this.options = options // 配置參數
this.prohibitPull = false // 禁止上下拉動操作標記
this.startY = 0 // 起始y坐標
this.startX = 0 // 起始x坐標
this.isBottom = false // 是否在底部
this.disX = 0 // 滑動X差值
this.disY = 0 // 滑動Y差值
this.pullDownHtml = ele.querySelector('.pullDownHtml')
this.pullUpHtml = ele.querySelector('.pullUpHtml')
this.pullDownHtmlHeight = 0 // 下拉的html高度
this.pullUpHtmlHeight = 0 // 上拉的html高度
this.left = 0 // 向左偏移量
// 初始化
this.init()
}
// 初始化
init () {
this.options.xThreshold = this.options.xThreshold || 0.25
// 設置樣式
this.ele.style.overflow = 'hidden'
this.ele.style.position = 'relative'
this.box.style.height = '100%'
this.box.style.width = this.containers.length * 100 + 'vw'
this.box.style.float = 'left'
this.box.style.transition = 'all ' + this.options.speed / 1000 + 's'
this.box.style.position = 'relative'
this.box.style.zIndex = 2
this.totalWidth = this.width * this.containers.length;;
[].forEach.call(this.containers, (ele) => {
ele.style.float = 'left'
ele.style.width = '100vw'
ele.style.height = '100%'
ele.style.overflow = 'auto'
ele.style.WebkitOverflowScrolling = 'touch'
ele.addEventListener('touchstart', (e) => {
this.startY = e.touches[0].clientY // 設置起始y坐標
this.startX = e.touches[0].clientX // 設置起始y坐標
}, false)
ele.addEventListener('touchmove', (e) => {
this.scrollTop = this.containers[this.nowIndex].scrollTop
this.isBottom = this.containers[this.nowIndex].querySelector('.s-pull').clientHeight <= this.scrollTop + this.height
// 判斷滑動方向是否為上下
const disY = e.touches[0].clientY - this.startY
const disX = e.touches[0].clientX - this.startX
// 設置事件(當為頂部或底部是取消默認事件)
if ((disY > 0 && ele.scrollTop == 0) || (disY < 0 && this.isBottom)) {
e.preventDefault()
}
// 若為左右滑動時取消默認事件
if (Math.abs(disY) < Math.abs(disX)) e.preventDefault()
}, false)
})
// 上下拉
if (this.options.isPullDown) {
this.pullDownHtml.style.position = 'absolute'
this.pullDownHtml.style.width = '100%'
this.pullDownHtmlHeight = this.pullDownHtml.clientHeight
}
if (this.options.isPullUp) {
this.pullUpHtml.style.position = 'absolute'
this.pullUpHtml.style.width = '100%'
this.pullUpHtml.style.bottom = '0'
this.pullUpHtmlHeight = this.pullUpHtml.clientHeight
}
// 添加事件
// 拖拽
touch.on(this.box, 'drag', (e) => {
this.direction = e.direction
this.touchmove(e)
this.options.onTouchmove && this.options.onTouchmove(this.nowIndex, e) // 事件輸出
})
// 滑動
!this.options.closeInertia && touch.on(this.box, 'swipe', (e) => {
this.swipe(e)
})
// 手指離開屏幕
touch.on(this.box, 'touchend', (e) => {
this.touchend(e)
})
// 移動至默認頁面
this.changePage(this.options.defaultPage || 0)
this.options.initCb && this.options.initCb()
}
// 拖拽方法
touchmove (e) {
this.box.style.transition = 'none' // 取消動畫
if ((e.direction === 'left' || e.direction === 'right') && !this.disY) {
// 左右滑動
this.disX = e.distanceX
changeTransform(this.box, (this.left + this.disX), this.disY)
} else if (!this.disX && !this.prohibitPull) {
// 上下滑動
if (e.direction === 'down' && !this.options.isPullDown) return
if (e.direction === 'up' && !this.options.isPullUp) return
if ((this.scrollTop <= 0 && this.direction === 'down') || (this.isBottom && this.direction === 'up')) {
// 上下拉動容器
this.disY = e.distanceY
changeTransform(this.box, (this.left + this.disX), this.disY)
}
}
}
// 手指離開屏幕
touchend (e) {
this.box.style.transition = 'all ' + this.options.speed / 1000 + 's' // 開啟動畫
if (!this.prohibitPull) {
if (Math.abs(this.disY) < this.options.threshold) { // 上下拉小於閥值自動復原
this.disY = 0
changeTransform(this.box, (this.left + this.disX), this.disY)
}
// 下拉刷新觸發
if (this.scrollTop <= 0 && this.direction === 'down' && this.disY >= this.options.threshold) {
this.disY = this.pullDownHtmlHeight
this.prohibitPull = true
// 顯示加載中
this.pullDownHtml.style.visibility = 'visible'
this.options.onRefreshStart && this.options.onRefreshStart(this.nowIndex) // 輸出下拉刷新事件
}
// 上拉加載觸發
else if (this.isBottom && this.direction === 'up' && Math.abs(this.disY) > this.options.threshold) {
this.disY = -this.pullUpHtmlHeight
this.prohibitPull = true
// 顯示加載中
this.pullUpHtml.style.visibility = 'visible'
this.options.onLoadStart && this.options.onLoadStart(this.nowIndex) // 輸出上拉事件
}
}
// 左右滑動
if (Math.abs(this.disX) < this.width * this.options.xThreshold) {
changeTransform(this.box, this.left, this.disY)
this.disX = 0
} else {
this.left += this.disX / Math.abs(this.disX) * this.width
if (this.left > 0) this.left = 0
if (this.left <= -this.totalWidth) this.left = -(this.totalWidth - this.width)
changeTransform(this.box, this.left, this.disY)
}
this.direction = '' // 重置方向
this.nowIndex = Math.abs(this.left) / this.width // 計算頁數
}
// 快速滑動
swipe (e) {
if (e.factor < 1 && !this.disX && !this.disY) {
if (e.direction === 'left') {
this.left -= this.width
} else if (e.direction === 'right') {
this.left += this.width
}
if (this.left > 0) this.left = 0
if (this.left <= -this.totalWidth) this.left = -(this.totalWidth - this.width)
changeTransform(this.box, this.left, this.disY)
}
this.disX = 0
this.nowIndex = Math.abs(this.left) / this.width // 計算頁數
}
// 關閉上下拉
pullEnd (cb) {
cb && cb(this.nowIndex)
changeTransform(this.box, this.left, 0)
this.disY = 0
this.prohibitPull = false
}
// 切換頁數
changePage (page) {
if (this.prohibitPull) return
this.left = -page * this.width
changeTransform(this.box, this.left, this.disY)
this.nowIndex = page
}
}
window.TabSwiper = TabSwiper
})(window, document)