移动端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文档
总结一下这次开发插件的原理和所遇到的坑吧
实现的话主要分为:
  1. 确定好容器结构
  2. 捕获滑动的事件(使用touch.js获取滑动的方向、滑动的距离和滑动的速度)
  3. 实现滑动的效果(这里使用的是transform来实现滑动, transtion来实现动画)
  4. 确定临界点(根据滑动不同的距离判断是否切换页数,还有根据滑动的速度确定是否切换页数)
  5. 暴露监听事件(产生不同状态的回调)
坑:

主要体现在微信和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)

移动端tab滑动和上下拉刷新加载的更多相关文章

  1. 分页插件思想:pc加载更多功能和移动端下拉刷新加载数据

    感觉一个人玩lol也没意思了,玩会手机,看到这个下拉刷新功能就写了这个demo! 这个demo写的比较随意,咱不能当做插件使用,基本思想是没问题的,要用就自己封装吧! 直接上代码分析下吧! 布局: & ...

  2. Android UI--自定义ListView(实现下拉刷新+加载更多)

    Android UI--自定义ListView(实现下拉刷新+加载更多) 关于实现ListView下拉刷新和加载更多的实现,我想网上一搜就一堆.不过我就没发现比较实用的,要不就是实现起来太复杂,要不就 ...

  3. Android Demo 下拉刷新+加载更多+滑动删除

    小伙伴们在逛淘宝或者是各种app上,都可以看到这样的功能,下拉刷新和加载更多以及滑动删除,刷新,指刷洗之后使之变新,比喻突破旧的而创造出新的,比如在手机上浏览新闻的时候,使用下拉刷新的功能,我们可以第 ...

  4. PullToRefresh下拉刷新 加载更多 详解 +示例

    常用设置 项目地址:https://github.com/chrisbanes/Android-PullToRefresh a. 设置刷新模式 如果Mode设置成Mode.PULL_FROM_STAR ...

  5. Android智能下拉刷新加载框架—看这些就够了

    一些值得学习的几个下拉刷新上拉加载开源库 Android智能下拉刷新框架-SmartRefreshLayout 支持所有的 View(AbsListView.RecyclerView.WebView. ...

  6. listView下拉刷新加载数据

    这个下拉效果在网上最早的例子恐怕就是Johan Nilsson的实现,http://johannilsson.com/2011/03/13/android-pull-to-refresh-update ...

  7. ListView实现上拉下拉刷新加载功能

    第一步.首先在你项目中创建一个包存放支持下拉刷新和上拉加载的类:

  8. uni-app下拉刷新加载刷新数据

    onPullDownRefresh监听该页面用户下拉刷新事件需要在 pages.json 里开启 enablePullDownRefresh "globalStyle": { } ...

  9. Android-PullToRefresh上拉下拉刷新加载更多,以及gridview刷新功能的Library下载地址

    作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985,转载请说明出处. 首先大家应该都听说过此开源框架的强大之处,支持单列以及双列的 上拉加载以及下拉刷新功 ...

随机推荐

  1. display 的 32 种写法

    从大的分类来讲, display的 32种写法可以分为 6个大类,再加上 1个全局类,一共是 7大类: 外部值 内部值 列表值 属性值 显示值 混合值 全局值 外部值 所谓外部值,就是说这些值只会直接 ...

  2. Django在form提交CSRF验证失败. 相应中断问题

    CSRF验证失败. 相应中断. 1).首先,我们可以先看一下出现问题的所在的原因. Your browser is accepting cookies. The view function passe ...

  3. spring cron 定时任务

    文章首发于个人博客:https://yeyouluo.github.io 0 预备知识:cron表达式 见 <5 参考>一节. 1 环境 eclipse mars2 + Maven3.3. ...

  4. Eventlog控件的使用

    CreateEventSource 已重载. 建立一个能够将事件信息写入到系统的特定日志中的应用程序. Delete 已重载. 移除日志资源. DeleteEventSource 已重载. 从事件日志 ...

  5. Spring Data JPA 入门Demo

    什么是JPA呢? 其实JPA可以说是一种规范,是java5.0之后提出来的用于持久化的一套规范:它不是任何一种ORM框架,在我看来,是现有ORM框架在这个规范下去实现持久层. 它的出现是为了简化现有的 ...

  6. java 集合框架(四)Set

    一.概述 Set是一种没有重复元素的集合,它所有的方法都是直接继承自Collection接口,并且添加了一个对重复元素的限制.Set要求强化了equals和hashCode两个方法,以使Set集合可以 ...

  7. JavaScript递归原理

    JavaScript递归是除了闭包以外,函数的又一特色呢.很多开发新手都很难理解递归的原理,我在此总结出自己对递归的理解. 所谓递归,可以这样理解,就是一个函数在自身的局部环境里通过自身函数名又调用, ...

  8. 1.5 PCI-X总线简介

    PCI-X总线仍采用并行总线技术.PCI-X总线使用的大多数总线事务基于PCI总线,但是在实现细节上略有不同.PCI-X总线将工作频率提高到533MHz,并首先引入了PME(Power Managem ...

  9. Android Gradle项目Hotfix热修复技术的接入

    https://github.com/AItsuki/HotFix Issues MAC系统无法自动打包补丁,原因可能是路径分隔符问题 使用谷歌multidex分包后无法注入代码(开启multidex ...

  10. freemarker报错之四

    1.错误描述 五月 28, 2014 9:56:48 下午 freemarker.log.JDK14LoggerFactory$JDK14Logger error 严重: Template proce ...