scroll-view踩坑记录

最近在使用uniapp开发一个群聊的小程序,需要加载聊天历史的功能,因此使用scroll-view实现,但这个过程可谓是非常坎坷。

作为一个后端开发,本来就对前端的什么渲染,promise不太懂,所以总是在代码的执行时序上出问题,比如nextTick的使用时机,因此也犯了很多低级错误。

与传统的在底部加载更多不同,聊天历史需要在容器顶部追加数据,这就导致了一个问题,数据加载完成后会跳到容器顶部,我期望的是它能停留在当前位置,解决方案有以下

基本结构
 <scroll-view
      scroll-anchoring
      :scroll-top="amend"
      @scrolltoupper="scrolltoupper"
      :scroll-into-view="tagLocation"
      :upper-threshold="upperThreshold"
      class="scroll-view"
      scroll-y
      id="scroll-view"
    >
      <view class="flex flex-col-reverse" id="chat-content">
         <view v-for="(item, index) in chatRecord" :key="index" :id="`item-${item.id}`">    
         </view>
      </view>
 </scroll-view>

1. 给每个消息标签添加id,当数据渲染完成后,使用scroll-into-view定位,但这会导致页面闪烁,原因是数据渲染后先跳到容器顶部,然后又拉到scroll-into-view标记的id位置,页面渲染了两次。

2. 在请求数据前,记录内容盒子高度,数据渲染后,再查看内容盒子高度,然后两者相减,就是scroll-top的值,但效果和1一样,甚至不如1

3. 滚动容器上下反转,内容上下反转,如果有滚动条,再左右镜像。这种做法相当于传统的底部加载更多功能,数据渲染到容器的底部,数据渲染不会导致当前位置改变,但这种做法也有问题,鼠标滚轮是反向的,对pc用户不友好。

4. 官方给出了使用css overflow-anchor:auto;的方案。一开始使用了,发现不生效,于是乎没注意它,后来又详细了解了overflow-anchor:auto;这个功能,发现它在web端确实有用,既然官方提到了它,说明它在小程序是适用的,肯定是我的用法不对。如下

  • ① 使用overflow-anchor:auto;在完成数据渲染后,不要再使用定位,也就是不要使用scroll-top和scroll-into-view属性,否则又回到了1、2的情况
  • ② 当内容滑到了容器顶部,也就是滑到不能再往上滑了,在完成数据渲染后,会跳到容器顶部,overflow-anchor:auto;不生效,原因未知。
  • ③ 新数据追加到数组的首部无效的话,可以试试将新数据添加到数组的尾部,这样不会改变数组旧数据的索引。然后内容盒子添加display: flex; 和flex-direction: column-reverse; 让数据从下到上展示(当然接口的数据也应该反过来)

通过测试,如果避免以上情况,数据可以做到无感加载,类似于微信加载历史一样。所以要解决的问题是,如何做到内容滑到顶部之前把数据渲染出来,或者如何防止内容滑到顶部。

当你滑动微信记录时,你会发现,微信的滑动被限速了,猜测是为了给加载数据提供时间。但是小程序好像没有能限制滑动速度的方法。此路不通

我们无法避免内容被滑到顶部,特别在小程序里,加载历史是网络请求,时间无法预估。所以应该在数据渲染之前,判断当前是否滑到顶部,如果滑到顶部,向下微调1个像素,这样渲染后就不会跳到容器顶部了,

 //局部代码------------------------------------------------

//data.top表示容器视口顶部位于屏幕的y坐标
const query = uni.createSelectorQuery().in(instance.proxy);
 const all = Promise.all([
      new Promise((resolve) => {
        query 
          .select('#scroll-view')
          .boundingClientRect((data) => {
            resolve(data.top)
          })
          .exec()
      }),
      new Promise((resolve) => {
        query 
          .select('#chat-content')
          .boundingClientRect((data) => {
            resolve(data.top)
          })
          .exec()
      }),
    ])

//滚动容器的top值是恒定不变的,内容盒子的top随着滑动而改变,
//当内容盒子滑到顶部时,二者相等,滚到顶部时overflow-anchor:auto;失效。
//所以在渲染数据前,手动往下滚动1个像素微调一下,
//也就是 :scroll-top = amend 这里的随机数不可去掉,等到微调完毕再渲染数据
 all.then((topArr) => {
        let dif = Math.abs(topArr[0] - topArr[1])
        if (dif == 0) {
          amend.value = Math.random() / 2 + 1
        }
        nextTick(() => {
//微调完毕页面刷新后再更新数据,否则不生效
          update() //向数组中加数据
         
        })
  })

其实这个方案也是不完美的,

        ①如果用户滑到顶部,但不松手,此时容器的top == 内容的top,且无法通过amend修正,也就是 :scroll-top = amend是不生效的,我试图在渲染数据时阻断用户触摸,但没找到对应的api,即使在请求数据时添加蒙层也不行,蒙层只能阻止蒙层触发后的触摸,没法切断蒙层触发之前的触摸。这种情况把触顶阈值设大点一般不会出现,除非接口特别慢。

        ②如果滑的非常快,接口请求也非常快,偶尔也会导致overflow-anchor:auto;失效, 我猜测在判断是否滑到顶部时为false,也就是if (dif == 0)时是false,但渲染时变成true了,因为滑的太快了。这里只能通过限制滑动速度解决,像微信那样,然而我没有找到这种api。退而求其次,就是尽量让一次请求的数据多一些,然后触顶事件的阈值大一些,尽量在滑到顶部之前就渲染完新的数据。也就是upper-threshold设的大点,我的upper-threshold设的动态值,会计算新渲染的数据的高度,然后乘以一个百分数,比如设的0.8,这样在滑动新数据的20%时我就开始请求下一页数据了。

const selectorQuery = (fun) => {
  const query = uni.createSelectorQuery().in(instance.proxy)
  query
    .select('#chat-content')
    .boundingClientRect((data) => {
      fun(data)
    })
    .exec()
}

//触顶事件


let just = 0 //请求数据之前的总高度
const upperThreshold = ref(200) //绑定:upper-threshold
const loadHistory = async () => {
    selectorQuery(async (data) => {
      just = data.height
//网络请求中。。。。。。。
      await queryHistory(......) //这里会把新的数据追加到列表数组里
      nextTick(() => {
        selectorQuery((data) => {
//新数据的高度 = 新数据渲染完后的总高度 - 之前的高度, 再乘以0.8是触发触顶事件的阈值
          let res = (data.height - just) * 0.8
          if (res < 1) {
            res = 1
          }
          upperThreshold.value = res
        })
      })
    })
}

最终实现的效果可以查看微信小程序:  一键财税甩单

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值