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
イベントを監視し、minScrollPos
とmaxScrollPos
を計算します。
アルゴリズムは非常に単純で、を計算して、子アイテムの高さを基にmaxScrollPos
を計算します。
このアルゴリズムは、デフォルトの子アイテムの高さがすべて同じであることを前提としています。
このアルゴリズムは正しい結果を出すはずですが、誤差が発生するということは、子アイテムの高さが同じではないことを意味します。
問題はどこにあるのでしょうか?
3. 今は、幻想の時間です#
画像タイプの子アイテムの高さを設定すると、実際の高さと指定した高さが一致しないことに気づきました。
style
属性で手動で指定しても効果がありません。
それでは、ブラウザの問題でしょうか?
Mozilla Foundation の公式ウェブサイトで img タグに関するドキュメントを見つけました。
250x250 の画像の外側に<div>
を手動で追加しました。
結果は以下の通りです:
高さが神秘な5.61px
増えました。これはブラウザに PR を提供できるかもしれませんね?
しかし、他のブラウザでも同様の結果が得られることがわかりました。結果は、これが私の見落としであることを示しています。
では、この神秘な数字はどこから来たのでしょうか?
4. 小さなフォントは本当に奇妙です#
line-height
を調整すると、この値が変化することがわかります。
ここで、この値はline-height
の変化に応じて変化します。
ここで、line-height
はフォントの高さに関連しており、文字と要素の底部の間にはbaseline
からline-height
までの距離があります。
line-height
は複数行のテキストの間隔を空けるために使用され、この高さは複数行のテキストの間隔を空けるために使用されるdescent
の間隔でしょうか?
コンテナをdisplay: flex;
またはdisplay: grid;
に変更すると、この神秘な間隔が消えることがわかります。
ここまで来れば、答えはほぼ明らかです。
5. マスター、私は悟りました#
この余分な間隔は、画像がdisplay: inline;
レイアウトで複数行のテキストのために空けられるものです。
この間隔の数値は、さまざまなブラウザで異なるものです。これは、JavaScript の計算精度やフォントの違いに関連している可能性があります。
ある大佬がGoogle フォントのベースライン係数の比較を専門に行っています。
結論#
デジタルの世界には、すべてのピクセルの裏には無数の細部があります。
新しい発見でもなく、コミュニティに新しい知識を提供するわけでもありませんが、問題解決のプロセスはとても楽しかったです。
これだけあれば十分です :)