移动端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文档
总结一下这次开发插件的原理和所遇到的坑吧
实现的话主要分为:
  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.box.clientWidth;

      [].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 = Math.abs(this.left) / this.width
    }
  }
  window.TabSwiper = TabSwiper
})(window, document)

原文地址:https://www.cnblogs.com/suyuanli/p/8589358.html

时间: 2024-08-11 01:45:33

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

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

http://blog.csdn.net/wwj_748/article/details/12512885 Android UI--自定义ListView(实现下拉刷新+加载更多) 关于实现ListView下拉刷新和加载更多的实现,我想网上一搜就一堆.不过我就没发现比较实用的,要不就是实现起来太复杂,要不就是不健全的.因为小巫近期要开发新浪微博客户端,需要实现ListView的下拉刷新,所以就想把这个UI整合到项目当中去,这里只是一个demo,可以根据项目的需要进行修改. 就不要太在乎界面了哈:

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

小伙伴们在逛淘宝或者是各种app上,都可以看到这样的功能,下拉刷新和加载更多以及滑动删除,刷新,指刷洗之后使之变新,比喻突破旧的而创造出新的,比如在手机上浏览新闻的时候,使用下拉刷新的功能,我们可以第一时间掌握最新消息,加载更多是什么nie,简单来说就是在网页上逛淘宝的时候,我们可以点击下一页来满足我们更多的需求,但是在手机端就不一样了,没有上下页,怎么办nie,方法总比困难多,细心的小伙伴可能会发现,在手机端中,有加载更多来满足我们的要求,其实加载更多也是分页的一种体现.小伙伴在使用手机版QQ

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

一些值得学习的几个下拉刷新上拉加载开源库 Android智能下拉刷新框架-SmartRefreshLayout 支持所有的 View(AbsListView.RecyclerView.WebView....View) 和多层嵌套的视图结构 支持自定义并且已经集成了很多炫酷的 Header 和 Footer (图). 支持和ListView的同步滚动 和 RecyclerView.AppBarLayout.CoordinatorLayout 的嵌套滚动 NestedScrolling. 支持在An

listview 下拉刷新加载数据

点击头条,头条会变成以下: 然后,过一段时间,刷新完成以后,listview又setSelection(1),增加一条数据,同时,把顶部给遮挡住: 这是点击刷新,然后是下拉刷新: 最后结果和点击刷新相同.那现在开始看下代码: 首先看下所用到的控件和变量: // 状态 private static final int TAP_TO_REFRESH = 1;//点击刷新 private static final int PULL_TO_REFRESH = 2;  //拉动刷新 private sta

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

第一步.首先在你项目中创建一个包存放支持下拉刷新和上拉加载的类: 第二步.需要把两个动画导入进来,实现180度旋转与360度旋转: 第三步.需要把支持的下拉与上拉显示的隐藏加载布局给导入进来 第四步.需要添加strings.xml与colors.xml文件的内容添加到项目里面: strings.xml <string name="pull_to_refresh">下拉刷新</string> <string name="release_to_ref

mui 上拉刷新加载template数据

html没什么好说的,就是主要刷新列表要套多套一层,要不动画会不见 //待刷新区域 <div id="cx_lst" style="margin-top: 30px;"> <div id="cx_lst2"> </div> </div> //临时数组var cx = [{ "ypmc": "11", "jg": 100, "id&

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

onPullDownRefresh 监听该页面用户下拉刷新事件 需要在 pages.json 里 开启 enablePullDownRefresh "globalStyle": { } 当处理完数据刷新后,uni.stopPullDownRefresh 可以停止当前页面的下拉刷新 uni.startPullDownRefresh(OBJECT) success Function 否 接口调用成功的回调 fail Function 否 接口调用失败的回调函数 complete Funct

Android打造(ListView、GridView等)通用的下拉刷新、上拉自动加载的组件

前言 下拉刷新组件在开发中使用率是非常高的,基本上联网的APP都会采用这种方式.对于开发效率而言,使用获得大家认可的开源库必然是效率最高的,但是不重复发明轮子的前提是你得自己知道轮子是怎么发明出来的,并且自己能够实现这些功能.否则只是知道其原理,并没有去实践那也就是纸上谈兵了.做程序猿,动手做才会遇到真正的问题,否则就只是自以为是的认为自己懂了.今天这篇文章就是以自己重复发明轮子这个出发点而来的,通过实现经典.使用率较高的组件来提高自己的认识.下面我们就一起来学习吧. 整体布局结构      

Android之RecyclerView轻松实现下拉刷新和加载更多

今天研究了下RecyclerView的滑动事件,特别是下拉刷新和加载更多事件,在现在几乎所有的APP显示数据列表时都用到了.自定义RecyclerView下拉刷新和加载更多听上去很复杂,实际上并不难,只要是对滑动事件的监听和处理. 一.自定义RecyclerView实现下拉刷新和加载更多 1.如何判断RecyclerView是在上滑还是下滑 在RecyclerView的OnScrollListener滑动事件监听中有个好用的方法,就是onScrolled(RecyclerView recycle