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>であり、1 つだけ<img>です。

そして、不思議なことが起こりました。元々正しくスクロールしていた要素が突然整列しなくなりました!これはスタイルの間違いですか?

過程#

1. 大佬も間違うことがあるのか?#

F12 を見てみると、better-scroll はtranslateZを使用していることが確認できました。子要素の高さは整数ですが、最後にスクロールすると、translateZで指定された値が小数になってしまいます。これは明らかに問題があります!F12 でtranslateZの値を手動で予想される整数に変更すると、問題が解決しました。しかし、これではダメです。次回スクロールするとまた同じ問題が発生します。そこで、scrollToをデバッグするために Sources パネルでブレークポイントを設定しました。

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を使用してコンテナ内の固定高さの子アイテムをスクロールしています。実際の範囲は以下のように計算されます。

// 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を計算します。
アルゴリズムは非常に単純で、コンテナの高さ子アイテムの数\frac{コンテナの高さ}{子アイテムの数}を計算して、子アイテムの高さを基にmaxScrollPosを計算します。
このアルゴリズムは、デフォルトの子アイテムの高さがすべて同じであることを前提としています。
このアルゴリズムは正しい結果を出すはずですが、誤差が発生するということは、子アイテムの高さが同じではないことを意味します。
問題はどこにあるのでしょうか?

3. 今は、幻想の時間です#

画像タイプの子アイテムの高さを設定すると、実際の高さと指定した高さが一致しないことに気づきました。
style属性で手動で指定しても効果がありません。
それでは、ブラウザの問題でしょうか?

Mozilla Foundation の公式ウェブサイトで img タグに関するドキュメントを見つけました。
250x250 の画像の外側に<div>を手動で追加しました。
結果は以下の通りです:

image

高さが神秘な5.61px増えました。これはブラウザに PR を提供できるかもしれませんね?
しかし、他のブラウザでも同様の結果が得られることがわかりました。結果は、これが私の見落としであることを示しています。

では、この神秘な数字はどこから来たのでしょうか?

4. 小さなフォントは本当に奇妙です#

line-heightを調整すると、この値が変化することがわかります。
ここで、この値はline-heightの変化に応じて変化します。
ここで、line-heightはフォントの高さに関連しており、文字と要素の底部の間にはbaselineからline-heightまでの距離があります。
line-heightは複数行のテキストの間隔を空けるために使用され、この高さは複数行のテキストの間隔を空けるために使用されるdescentの間隔でしょうか?

image

ベースライン&行の高さ&垂直方向の配置の秘密を解き明かす

コンテナをdisplay: flex;またはdisplay: grid;に変更すると、この神秘な間隔が消えることがわかります。

image

ここまで来れば、答えはほぼ明らかです。

5. マスター、私は悟りました#

この余分な間隔は、画像がdisplay: inline;レイアウトで複数行のテキストのために空けられるものです。
この間隔の数値は、さまざまなブラウザで異なるものです。これは、JavaScript の計算精度やフォントの違いに関連している可能性があります。

ある大佬がGoogle フォントのベースライン係数の比較を専門に行っています。

image

結論#

デジタルの世界には、すべてのピクセルの裏には無数の細部があります。
新しい発見でもなく、コミュニティに新しい知識を提供するわけでもありませんが、問題解決のプロセスはとても楽しかったです。
これだけあれば十分です :)

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。