banner
sora

sora

编程心得 & 人文感悟

从一次滚动不对齐引发的惨案

P.S. 发现已经有人研究过这个问题了
把链接放在这边吧
https://www.zhangxinxu.com/wordpress/2015/08/css-deep-understand-vertical-align-and-line-height/

背景#

这个问题是在使用大佬的 better-scroll 库实现类似 iPhone 的选择日期效果时发现的
在创建时指定 wheel 里面的容器和子元素
子元素大部分都是<p>,只有一个是<img>

然后神奇的事情发生了,原本好好的滚动到元素突然不对齐了!
这难道是样式写错了吗?

过程#

1. 大佬也有出错的时候?#

去看了 F12,确认 better-scroll 是使用translateZ 来实现的
子元素的高度都是整数,但滚动到最后,translateZ 指定的值居然是小数,这肯定有问题!
手动在 F12 修改translateZ的值为预期的整数,问题解决了
但这样肯定不行,下次滚动还是老样子
于是在 Sources 面板打断点调试scrollTo

2. 本身就不太对劲#

注意到在 better-scroll 的resetPosition方法中,滑动到底部后反弹是有一个最低 y 值限制的

// https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/core/src/scroller/Scroller.ts#L592
  resetPosition(time = 0, easing = ease.bounce) {
    const {
      position: x,
      inBoundary: xInBoundary,
    } = this.scrollBehaviorX.checkInBoundary()
    const {
      position: y,
      inBoundary: yInBoundary,
    } = this.scrollBehaviorY.checkInBoundary()

    if (xInBoundary && yInBoundary) {
      return false
    }

    /* istanbul ignore if  */
    if (isIOSBadVersion) {
      // fix ios 13.4 bouncing
      // see it in issues 982
      this.reflow()
    }
    // out of boundary
    this.scrollTo(x, y, time, easing)

    return true
  }

checkInBoundary中会用到一个this.adjustPosition
后者会把滚动的 x 轴和 y 轴位置约束在[this.minScrollPos, this.maxScrollPos]的范围内
this.maxScrollPos又是在computeBoundary函数中赋值
调试得到maxScrollPos也是一个小数
但按上面的算法,算出来是整数啊?

但是我们的情况是使用wheel在容器内固定高度的子 item 上滚动,实际的范围是在下面算的

// https://github.com/ustbhuangyi/better-scroll/blob/f87fbd161d4bebb1d1e90fc1675db1b0f90174d0/packages/wheel/src/index.ts#L189
    scrollBehaviorY.hooks.on(
      scrollBehaviorY.hooks.eventTypes.computeBoundary,
      (boundary: Boundary) => {
        this.items = this.scroll.scroller.content.children
        this.checkWheelAllDisabled()

        this.itemHeight =
          this.items.length > 0
            ? scrollBehaviorY.contentSize / this.items.length
            : 0

        boundary.maxScrollPos = -this.itemHeight * (this.items.length - 1)
        boundary.minScrollPos = 0
      }
    )

这里Wheel对象(迫真 “轮子”)监听 computeBoundary事件,计算minScrollPosmaxScrollPos
可以看到算法比较简单粗暴,就是通过计算 容器的高度item的个数\frac{容器的高度}{子item的个数} 得到子 item 的高度,据此计算maxScrollPos
这种算法能够正确,是建立在默认子 item 的高度都相同的基础上的
它出了误差,说明子 item 的高度并不相同
问题出在哪里呢?

3. 现在是,幻想时间#

注意到给图片类型的子 item 设置高度时,实际的高度与指定的高度是不一致的
就算我在style属性中手动指定,也无济于事
难道是浏览器的问题?

我找到了 mozilla 基金会官网关于 img 标签的文档
在 250x250 的图片外手动套了一层<div>
结果是这样的:

image

高度多出了神秘的5.61px,难道可以给浏览器提 PR 了?
但是找了一圈发现,其他浏览器类似,结果只能证明这是我的少见多怪,白高兴一场

那么这神秘的数字是从哪里来的呢?

4. 小小字体真奇妙#

调整line-height可以发现,这个数值会随着line-height变化
在这里我如果把line-height设置为10px,神秘的多余高度就消失了
我们知道line-height与字体高度有关,在字符和元素底部之间会有一个baselineline-height的距离
line-height是在多行文本之间留出差距,这个高度是不是就是给 “多行文本” 留出的间隔descent呢?

image

揭开 baseline & line-height & vertical-align 的面纱

将容器修改为display: flex; 或者 display: grid;
我们看到这个神秘的间隔消失了

image

到此,答案已经基本水落石出了

5. 大师,我悟了#

这个多余的间隔,正是图片在 display: inline; 布局时为多行文本留出的
间隔的数字在各个浏览器上不同,可能和 javascript 的计算精度有关,也可能和字体的不同有关

有位大佬专门做了Google 字体的 baseline 系数对比

image

结语#

数字世界每个像素背后,都是无数的细节
虽然算不上什么新发现,也没有为社区贡献新的知识,但是解决问题的过程很开心
有这点就够了 :)

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。