小程序自定义下拉刷新组件(为应对头部有非滚动区的情况)

文心雕刺

2021-03-21 update:
经过一个多月的测试实践,修复了这个组件初版的诸多不足,最重要的地方就是在ios上的自带的橡皮筋效果会影响到下拉效果,甚至某些老iphone会出现严重的抖动,还有滑动的时候如果出现类似点击的效果会导致滑动卡住的bug,这两个问题目前已经解决掉了,也是通过反复更改和测试的,由于更改内容较多,涉及css,wxml和js,暂时没有精力去更新,我正在考虑做成npm包。

2021-03-24:
现在已经将该组件发布到npm,包名为: hws-scroll

原文章:
效果动图:

image

小程序是有其下拉刷新api的,然而头部或者尾部有非滚动区域的情况下,是应对不了的,相关问题在微信开放社区已经是老生常谈了,比如:

onPullDownRefresh 下拉刷新在安卓手机上会带动头部fixed元素一起下拉的问题

又比如:

小程序下拉刷新onPullDownRefresh 问题,fixed定位下移!

像这种问题有很多,不一一列举了。

这个问题从2018年开始就不断有开发者向微信开放社区反馈,一直到现在,都没有解决,没办法,本公司项目中有头部是固定非滚动的列表页面,只得自己自定义一个。

列表页的上拉加载,参考了这篇文章中的方法,链接放在这里,大家可以移步去看一下:

浅谈微信小程序中的下拉刷新和上拉加载

回到主题,下拉刷新,我的思路是在这篇文章里找到的:

小程序开发踩坑记录(五)——模拟实现底部tabbar和下拉刷新功能(解决安卓端打开下拉刷新功能后fixed元素失效问题)

我一上来打算用该文章中的方法,来自制下拉刷新,可是我是在自定义组件中使用,不是在page中定义,在自定义组件中scroll-view的onscroll事件怎么也响应不了,也就无法获得当前页面的scrollTop。

后来我受这个启发,既然可以在tocuh事件中改变scroll-view的scrollTop,来模拟出一个下拉刷新区域的scrollTop随手指滑动而变化,进而实现其下拉显示和回弹,那我也可以使用绝对定位和相对定位,依靠改变容器的top值来实现这个功能,也就是可以将滚动容器的top初始值设置为负的刷新区域的高度,在tocuhstart和tocuhmove事件中获取手指在屏幕上的滑动距离,让容器的top和滑动距离呈正相关,当top值达到自己设置的阈值的时候停止变化,随后在tocuhend事件中发起刷新请求。

image

大体思路如上图所示

下面贴一下基本代码:


<view
  class="scroll"
  scroll-y
  style="min-height: {{pageHeight + 'px'}}; top: {{marginTop + 'rpx'}}"
  enable-back-to-top="{{true}}"
  bindtouchmove="movePull" 
  bindtouchstart="startPull" 
  bindtouchend="endPull" >
  <view class="scroll__content {{springbacking ? 'return__content' : ''}}" style="min-height: {{contentHeight + 'px'}};top: {{scrollTop + 'px'}}">
    <view class="refresh__wrap">
      <view class="refresh__center">
        <view class="refresh__icon {{refreshText === '松开刷新' ? 'rotate' : ''}}" >
          <mp-icon icon="sending" color="#999" size="{{35}}"></mp-icon>
        </view>
        <view class="tips">{{refreshText}}</view>
        <view class="times">最后更新:{{refreshTime}}</view>
      </view>
    </view>
    <slot />
    <view class="footer__line" wx:if="{{recycleList.length}}">
      <view class="scroll__loading" wx:if="{{bottomLoadingShow}}"></view>
      <text class="text {{bottomLoadingShow ? 'notline' : ''}}">{{lineText}}</text>
    </view>
  </view>
  <no-data 
    show="{{notDataShow}}"
    bind:refresh="refresh" />
</view>

重点说下,这个组件结构为外层.scroll元素,最小高度设置为pageHeight(计算方法后面会贴),position为relative,其top值设置为父级页面传入的marginTop值,就是父级页面头部非滚动元素的rpx高度,如果没有非滚动元素,则默认为0,这样的话,外层的位置就可以依赖marginTop值来确定了,滚动的时候不会把非滚动区域也算进去;
内层列表滚动区域.scroll__content元素,position为absolute最小高度contentHeight,top值scrollTop,初始-80px(刷新文案元素高度),这样就把.refresh__wrap刷新文案元素隐藏起来了。此外,设置一个默认插槽,就是列表循环内容。

pageHeight和contentHeight计算方法,用到wx.getSystemInfozhege api,比较简单:

getPageHeight() {
      const self = this;
      wx.getSystemInfo({
        success: (res) => {
          self.setData({
            pageHeight: res.windowHeight - (self.data.marginTop / 750 * res.windowWidth),
            contentHeight: (res.windowHeight - (self.data.marginTop / 750 * res.windowWidth)) + REFRESHHEIGHT + 2,
          })
        }
      })
    }

以上,marginTop是父级组件传进来的头部非滚动区域的rpx高度值(头部非滚动区域position设置为fixed),默认为0,REFRESHHEIGHT是80,就是刷新文案区域的高度px值,最后加2是保证在第一页铺不满整屏的情况下,能让onReachBottom上拉事件生效,上拉加载本文不多说了,可以直接去看上面贴出的链接:“浅谈微信小程序中的下拉刷新和上拉加载”(注意,我这里是自定义组件,onReachBottom事件同样要在父页面中写onReachBottom生命周期,来调用自定义组件中的onReachBottom方法)。

tocuhstart方法,就是获取手指开始接触屏幕的clentY值:

startPull(ev) {
  this.setData({ springbacking: false }); // 开始下拉的时候,去除.scroll的transition为0.4s的动画延迟效果,使其完全遵循手指的驱动
  this.lastTop = ev.changedTouches[0].clientY;
},

tocuhmove方法:

// MAX_MOVE_TOP 为 120 允许最大滑动距离
// MAX_SCROLL_TOP 为 20 允许.scroll__content的最大top值
movePull(ev) {
      this.nowY = ev.changedTouches[0].clientY;// 手指当前触摸位置的clentY值
      this.nowY = this.nowY - this.lastTop;// 滑动距离
      const query = wx.createSelectorQuery();
      query.select('.scroll').boundingClientRect();
      query.selectViewport().scrollOffset();
      query.exec((rect) => { // 必须是滚动高度为0即在顶部的时候触发
        if (rect[1].scrollTop <= 0 && this.nowY > 0 && this.nowY <= MAX_MOVE_TOP && this.data.recycleList.length) {
          this.setData({// 满足以上条件的,则使.scroll__content元素的top值等于-80px 加上滑动距离 nowY
            scrollTop: -REFRESHHEIGHT + this.nowY,
          })
          if(this.nowY >= 100) {
            this.setData({
              refreshText: '松开刷新',
            })
          }
        }
      })

      if(this.nowY > MAX_MOVE_TOP && this.data.scrollTop < MAX_SCROLL_TOP && this.data.recycleList.length) {
        this.setData({// 此处判断是为了解决手指滑动过快,tocuhmove得到的clentY值呈非线性变化,导致滑动距离可能上一次还是100以内,下一次直接就到300开外,无法满足上面的top变化条件,就卡住了。所以此时手动将.scroll__content的top值设置为20。
          refreshText: '松开刷新',
          scrollTop: MAX_SCROLL_TOP,
        })
      }
      ...
      // 上拉加载逻辑
      ...
    }

以上,下拉刷新都是在进入页面有数据的情况下才会触发即this.data.recycleList.length这个条件,无数据的情况下则显示no-data组件,如下图所示,直接点击刷新按钮刷新即可。
image

tocuhend事件:

endPull() {// 结束滑动的时候当.scroll__content的top值大于等于20,则可以执行刷新方法。
      if(this.data.scrollTop >= MAX_SCROLL_TOP) {
        wx.showNavigationBarLoading();
        this.refresh();
      }
      ...
      // 上拉加载逻辑
      ...
      if (this.data.scrollTop > -REFRESHHEIGHT) {
        setTimeout(() => { // 定时器是防止因为手指离开屏幕过快(类似点击事件,但又下拉了一段距离),导致的数据更新,视图未更新,而卡住的情况。
          this.setData({ // 这个springbacking为true的时候,.scroll__content元素的transition就是0.4s,回弹时候的动画效果。
            springbacking: true,
            scrollTop: -REFRESHHEIGHT
          })
        }, 50)
      }
    },

好了,下拉刷新的逻辑就写完了,该下拉刷新在安卓和ios上的效果差别不是很大,由于业务需要,我是把他做成了一个组件,一些刷新方法和父页面的请求事件成功交互,失败交互,不在本文探讨范围之内,所以就略去了,想试试的朋友可以直接在页面中将slot插槽替换成列表循环元素尝试。

阅读 1.8k
138 声望
3 粉丝
0 条评论
你知道吗?

138 声望
3 粉丝
宣传栏