评论

小程序scroll-view翻转后 scroll-into-view的替代方案

小程序scroll-view翻转后 scroll-into-view的替代方案

背景

腾讯云医小程序有医患聊天会话的场景,由于会话场景存在查询历史消息的场景,小程序中按照常规思路加载历史消息时会出现跳动的问题;跳动的原因是由于在’顶部’插入dom,会使得后面的dom被往后面推,然后重新设置scroll-top或者scrol-into-view从而导页面出现跳动;我们尝试采用【 前端开发中聊天场景的体验优化】文章中的方案处理跳动的场景。该文章的核心观点将scroll-view元素通过设置css样式 transform: rotateX(180deg); 进行翻转,这样将历史消对应的dom结构放在尾部,当添加更多的历史消息(dom)时,由于dom是添加在尾部很优雅的绕过了插入历史消息跳动的场景。但是当我们按照这种方式实现后,发现scroll-view元素提供的scroll-into-view属性不好使了。因此有了本文通过计算scrollTop值设置scrollTop来达到相同目的。

复现该问题的小程序代码片段:代码片段

目前已经反馈给官方(官方已确认是内部组件实现暂不支持翻转的场景

基础知识介绍

计算scrollTop涉及到一些web和小程序的基础知识,后面针对这些基础点进行简单介绍

.scroll-into-view

微信小程序提供的scroll-view元素提供了属性 scroll-into-view,该属性的作用是可以将指定dom滚动到scroll-view可见区域内

关于boundingClientRect

下图是MDN解释该属性时提供的,从下图中可以看到top/bottom/left/right的值是元素的左上角和右下角相对于视口左上角的水/垂直距离

为了更深入理解这些值。给出了一个简易的demo(代码片段),获取实例元素的的boundingClientRect的值后,可以看到这些值是根据元素的border边界进行计算的



  • 值得注意的是,当元素处于一个滚动区域内部,left/top值是考虑滚动操作的即包含滚动距离的(参考MDN
  • 另外,当我们把容器元素又或者元素自身设置 transform: rotateX/Y(180deg):不会导致top和bottom的值互换(left与right的值互换);

总会有这样的结论,当dom元素的宽度和高度不为0时,top值一定小于bottom值,left值一定小于right值

关于scrollTop

当一个容器的内容的高度大于其容器高度时,overflow不为visible/hidden时,则会出现滚动条。出现滚动条后,内容区域则可以滚动,此时scrollTop的值是容器可视区域的顶部到内容区域顶部的距离,见下面示意图。

值得注意的是,滚动条出现在盒模型中的content区域,见下图滚动条不会覆盖padding/border部分。因此上面说到内容区域高度超过容器高度并不严谨,严谨的说法应该是超过容器的content区域的高度。

如果此时给容器设置css样式: transform: rotateX(180deg); 即沿垂直方向翻转180度,scrollTop的值会发生变化吗。

下面我们看下实际的对比效果,为了方便查看滚动条的效果,给滚动条轨道(红色部分)以及滑块(黑色部分)添加了背景色,发现整个元素包括滚动条在内一并进行了翻转。

正常情况(左侧),应用翻转css样式(右侧)

翻转后的scrollTop值示意图

通过计算scrollTop值来模拟scroll-into-view效果(针对scroll-view翻转的场景j)

  1. 由于boundingClinetRect的值是包含border边界的,因此当数据项包含padding,border等区域不会影响这里的计算过程,可以认为下面示意图中的数据项部分的边界是border边界;

  2. 由于滚动条是出现在content区域,因此容器元素的的border-top/padding-top不为0时,会影响计算流程,因此这里分为两种情况进行介绍:

    2.1 假设scroll-view元素的的border-top/padding-top为0

    2.2 假设scroll-view元素的的border-top/padding-top不为0

border-top/padding-top为0的情况

为了方便说明计算过程,我定义三种状态,初始态、中间态、最终态

示意图中的区域说明

  • 白色背景的为视口,
  • 绿色背景的是容器(scroll-view)的可视区域,
  • 灰色区域是内容区域,并且内容区域的高度超过了容器的高度,
  • 红色区域是一个数据项

现在的目标是将数据项从初始态滚动到最终态即scroll-into-view的效果:border的上边界与可视区域上边界对齐

第一步:从初始态达到中间态

根据上面关于scrollTop的描述,这里如果scrollTop的值是targetDistance即数据项的底部到内容区域的底部的距离,就可以达到中间态,因此现在的目标是求targetDistance

初始状态的已知变量

  • 初始状态下的的scrollTop值:currentScrollTop (由于容器发生翻转,所以scrollTop视觉上指向容器下方)
  • 数据项的boundingClientRect.bottom为 itemBottom
  • 容器的boundingClientRect.bottom为 contianerBottom

通过示意图很容易得出

targetDistance = currentScrollTop + (containerBottom - itemBottom)

第二步:从中间到达最终态

已知变量:容器高度:containerHeight、数据项高度:itemHeight

最终态是数据项的顶部距离容器顶部,从示意图中看到中间态到最终态的scrollTop是减少了的,减少的值其实就是cotainerHeight - itemHeight

经过第一步和第二步我们就可以得到scrollTop的计算公式

let itemScrollTop = currentScrollTop + containerBottom - itemBottom​
itemScrollTop -= (containerHeight - itemHeight)​ 

=> itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight)

border-top/padding-top不为0的情况

根据上面第一种情况的介绍的思路,很容易得到下面结果,不再赘述(X 就是容器padding-top + border-top的值)

let itemScrollTop = currentScrollTop + containerBottom - itemBottom - X
itemScrollTop -= (containerHeight - itemHeight - X) 

=> itemScrollTop = currentScrollTop + containerBottom - itemBottom - X - (containerHeight - itemHeight - X)

=> itemScrollTop = currentScrollTop + containerBottom - itemBottom - (containerHeight - itemHeight)

【结论】两种情况最终的计算过程是一样的,因此在实现的过程中不需要进行区分

代码实现

代码片段见:https://developers.weixin.qq.com/s/y1X11dmr7AqC

视图层代码

{{item.content}}
​
#scroll-view {
    position: absolute;
    top: 50px;
    bottom: 50px;
    width: 100%;
    background-color: rgba(0, 0, 0, 0.1); // 关键case
    transform: rotateX(180deg);
}

逻辑层核心代码

scrollTo () {
  const itemId = '#item_id_50'
  const containerId = '#scroll-view'
  Promise.all([this._queryBoundingClient(itemId), this._getScrollInfo(containerId)])
    .then((res = [[[{}]], {}]) => {
      const [[[
        { bottom: itemBottom, height: itemHeight }]],
        { bottom: containerBottom, scrollTop, height: containerHeight }] = res
      let itemScrollTop = containerBottom - itemBottom + scrollTop
      itemScrollTop -= (containerHeight - itemHeight)
      this.setData({ scrollTop: itemScrollTop })
    })
},
_queryBoundingClient (selector) { // 获取目标dom的相关位置/尺寸信息
  return new Promise(resolve => {
    const query = this.createSelectorQuery();
    query.selectAll(selector).boundingClientRect();
    query.exec(resolve);
  })
},
_getScrollInfo (idSelector) { // 用来获取容器层相关位置/尺寸信息
  return new Promise(resolve => {
    const query = this.createSelectorQuery()
    query.select(idSelector).boundingClientRect()
    query.select(idSelector).scrollOffset()
    query.exec((res = [{}, {}]) => {
      const [{ top, bottom, height }, { scrollHeight, scrollTop }] = res
      const scrollInfo = { scrollTop, scrollHeight, top, bottom, height }
      resolve(scrollInfo)
    })
  })
}
最后一次编辑于  2023-03-23  
点赞 4
收藏
评论

6 个评论

  • 薄荷微光
    薄荷微光
    2022-04-25

    感谢分享,正好遇到 我们去试试

    2022-04-25
    赞同 1
    回复
  • 从君华
    从君华
    2023-09-18

    最近遇到scroll-view的一个问题:不论是 scroll-into-view 还是手动控制滚动的位置都有问题,具体表现为从前往后滚动正常,往回滚异常(iOS异常,工具和Android都正常)。

    scroll-into-view 异常? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000c801a278400a3ca10f11946b000

    2023-09-18
    赞同
    回复
  • A3
    A3
    2023-07-13

    感谢余松,靠谱的方案,完美解决问题

    2023-07-13
    赞同
    回复
  • 黄思程
    黄思程
    2023-05-24

    现在可以用 scroll-view reverse 属性来实现

    2023-05-24
    赞同
    回复 3
  • Apink
    Apink
    2022-07-21

    再次加载数据的时候,页面空白

    2022-07-21
    赞同
    回复
  • 🦉皮皮峰🦉
    🦉皮皮峰🦉
    2022-05-18

    使用改方案,有新数据渲染时,多次触摸后滑动会向相反方向,这个有遇到吗

    2022-05-18
    赞同
    回复 1
    • ZORA
      ZORA
      2023-11-03
      请问处理了么?
      2023-11-03
      回复
登录 后发表内容