背景
腾讯云医小程序有医患聊天会话的场景,由于会话场景存在查询历史消息的场景,小程序中按照常规思路加载历史消息时会出现跳动的问题;跳动的原因是由于在’顶部’插入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)
-
由于boundingClinetRect的值是包含border边界的,因此当数据项包含padding,border等区域不会影响这里的计算过程,可以认为下面示意图中的数据项部分的边界是border边界;
-
由于滚动条是出现在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)
})
})
}
感谢分享,正好遇到 我们去试试
最近遇到scroll-view的一个问题:不论是 scroll-into-view 还是手动控制滚动的位置都有问题,具体表现为从前往后滚动正常,往回滚异常(iOS异常,工具和Android都正常)。
scroll-into-view 异常? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000c801a278400a3ca10f11946b000
感谢余松,靠谱的方案,完美解决问题
现在可以用 scroll-view reverse 属性来实现
再次加载数据的时候,页面空白
使用改方案,有新数据渲染时,多次触摸后滑动会向相反方向,这个有遇到吗