移动端tab滑动和上下拉刷新加载
移动端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)
移动端tab滑动和上下拉刷新加载的更多相关文章
- 分页插件思想:pc加载更多功能和移动端下拉刷新加载数据
感觉一个人玩lol也没意思了,玩会手机,看到这个下拉刷新功能就写了这个demo! 这个demo写的比较随意,咱不能当做插件使用,基本思想是没问题的,要用就自己封装吧! 直接上代码分析下吧! 布局: & ...
- Android UI--自定义ListView(实现下拉刷新+加载更多)
Android UI--自定义ListView(实现下拉刷新+加载更多) 关于实现ListView下拉刷新和加载更多的实现,我想网上一搜就一堆.不过我就没发现比较实用的,要不就是实现起来太复杂,要不就 ...
- Android Demo 下拉刷新+加载更多+滑动删除
小伙伴们在逛淘宝或者是各种app上,都可以看到这样的功能,下拉刷新和加载更多以及滑动删除,刷新,指刷洗之后使之变新,比喻突破旧的而创造出新的,比如在手机上浏览新闻的时候,使用下拉刷新的功能,我们可以第 ...
- PullToRefresh下拉刷新 加载更多 详解 +示例
常用设置 项目地址:https://github.com/chrisbanes/Android-PullToRefresh a. 设置刷新模式 如果Mode设置成Mode.PULL_FROM_STAR ...
- Android智能下拉刷新加载框架—看这些就够了
一些值得学习的几个下拉刷新上拉加载开源库 Android智能下拉刷新框架-SmartRefreshLayout 支持所有的 View(AbsListView.RecyclerView.WebView. ...
- listView下拉刷新加载数据
这个下拉效果在网上最早的例子恐怕就是Johan Nilsson的实现,http://johannilsson.com/2011/03/13/android-pull-to-refresh-update ...
- ListView实现上拉下拉刷新加载功能
第一步.首先在你项目中创建一个包存放支持下拉刷新和上拉加载的类:
- uni-app下拉刷新加载刷新数据
onPullDownRefresh监听该页面用户下拉刷新事件需要在 pages.json 里开启 enablePullDownRefresh "globalStyle": { } ...
- Android-PullToRefresh上拉下拉刷新加载更多,以及gridview刷新功能的Library下载地址
作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985,转载请说明出处. 首先大家应该都听说过此开源框架的强大之处,支持单列以及双列的 上拉加载以及下拉刷新功 ...
随机推荐
- spring 运行时属性值注入
继续spring学习,今天介绍两种外部属性值注入的方式.当你需要读取配置信息时,可以快速读取. 开始之前先创建属性文件site.properties,放在classpath下面 #数据库配置 ### ...
- js的继承实现
1.原型链继承 1.创建父类对象2.创建子类函数对象3.将父类的实例对象赋值给子类的原型4.将子类的原型属性的构造函数设置为 子类本身 function Person(name) { this.nam ...
- 老男孩Python全栈开发(92天全)视频教程 自学笔记09
day9课程内容: 乌班图(ubuntu)64位系统 和 VMware 虚拟机安装(官网收费又麻烦,在网上找资源 安装vmware: vm运行(秘钥找度娘)--文件--新建虚拟机--自定义 下一步-- ...
- 《设计模式之禅》--MVC框架
需求:设计一个MVC框架 (以下可能摘要不全,后期整理) 架构图: * 核心控制器:MVC框架入口,负责接收和反馈HTTP请求 * 过滤器:Servlet容器内的过滤器,实现对数据的过滤处理 * 拦截 ...
- Spring 框架系列之 JDBC 整合实例
微信公众号:compassblog 欢迎关注.转发,互相学习,共同进步! 有任何问题,请后台留言联系! 1.Spring框架整合 DAO 模板 JDBC:org.springframework.jdb ...
- Elasticsearch安装使用
在网上有很多那种ES步骤和问题的解决 方案的,不过没有一个详细的整合,和问题的梳理:我就想着闲暇之余,来记录一下自己安装的过程以及碰到的问题和心得:有什么不对的和问题希望及时拍砖. 第一步:环境 li ...
- java堆内存详解
http://www.importnew.com/14630.htmljava堆的特点<深入理解java虚拟机>是什么描述java堆的 Java堆(Java Heap)是java虚拟机所管 ...
- ACL访问控制列表
acl是基于文件系统的,所以支不支持acl在于使用什么文件系统. FAT32文件系统不支持权限,也不区分大小写 如果一个分区不是安装系统时分的分区,是一个新的分区的话,默认是不支持acl CentOS ...
- H3C三层交换机配置IP
1.直接在物理端口上设置IP地址. int f1/0/1 port link-mode route #链路模式采用路由 ip add 192.168.10.1 24 ospf network0type ...
- Vmware下centos与windows能ping通并能上网
1.桥接模式 2.NAT模式 3.Host-Only模式 1.桥接模式 vim /etc/udev/rules.d/70-persistent-net.rules 与/etc/sysconfig/ne ...