P.S. It has been found that someone has already researched this issue. Let's put the link here: https://www.zhangxinxu.com/wordpress/2015/08/css-deep-understand-vertical-align-and-line-height/
Background#
This issue was discovered when using the better-scroll library by a master to achieve a date selection effect similar to the iPhone. When creating, specify the container and child elements in the wheel.
Most of the child elements are <p>
, with only one being <img>
.
Then something magical happened, the scrolling to the elements that were originally aligned suddenly became misaligned!
Could this be a mistake in the styles?
Process#
1. Even masters make mistakes?#
Checked F12 and confirmed that better-scroll uses translateZ
to implement it.
The heights of the child elements are all integers, but when scrolling to the end, the value specified by translateZ
is actually a decimal, which is definitely a problem!
Manually modified the value of translateZ
to the expected integer in F12, and the problem was solved.
But this is definitely not a solution, it will still be the same next time scrolling.
So I set a breakpoint in the Sources panel to debug scrollTo
.
2. Something is not quite right from the beginning#
Noticed that in the resetPosition
method of better-scroll, there is a minimum y value limit for bouncing after scrolling to the bottom.
// 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
}
In checkInBoundary
, it uses this.adjustPosition
, which constrains the scrolling x and y positions within the range [this.minScrollPos, this.maxScrollPos]
.
this.maxScrollPos
is assigned in the computeBoundary
function.
Debugging shows that maxScrollPos
is also a decimal.
But according to the above algorithm, it should be an integer, right?
However, in our case, when scrolling on fixed-height child items in the container using wheel
, the actual range is calculated below:
// 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
}
)
Here, the Wheel
object listens for the computeBoundary
event and calculates minScrollPos
and maxScrollPos
.
You can see that the algorithm is relatively simple, it calculates the height of the child item based on the calculation of , and calculates maxScrollPos
based on this.
This algorithm can work correctly if the default heights of the child items are the same.
If there is an error, it means that the heights of the child items are not the same.
So where is the problem?
3. Now, it's time for imagination#
Noticed that when setting the height for the image type child item, the actual height is different from the specified height.
Even if I manually specify it in the style
attribute, it doesn't work.
Could it be a browser issue?
I found the Mozilla Foundation's documentation on img tags.
I wrapped a 250x250 image with an extra <div>
.
The result is like this:
The height is increased by a mysterious 5.61px
. Can I submit a PR to the browser for this?
But after searching around, I found that other browsers have similar results, which means that this is just my imagination and I got excited for nothing.
So where does this mysterious number come from?
4. The wonders of small fonts#
By adjusting the line-height
, it can be observed that this value changes with the line-height
.
If I set the line-height
to 10px
here, the mysterious extra height disappears.
We know that line-height
is related to the height of the font. There is a distance from the baseline
to the line-height
between characters and the bottom of the element.
line-height
creates gaps between multiple lines of text. Could this height be the spacing descent
left for "multiple lines of text"?
Unveiling the mystery of baseline & line-height & vertical-align
If the container is modified to display: flex;
or display: grid;
,
we can see that this mysterious spacing disappears.
At this point, the answer is basically clear.
5. Master, I understand#
This extra spacing is precisely the spacing left for multiple lines of text when the image is laid out with display: inline;
.
The value of the spacing is different in various browsers, which may be related to the precision of JavaScript calculations or different fonts.
A master has specifically done a comparison of the baseline coefficient of Google Fonts: Google Fonts Baseline Coefficient Comparison
Conclusion#
Behind every pixel in the digital world, there are countless details.
Although it is not a new discovery and does not contribute new knowledge to the community, I am happy with the process of solving the problem.
Having this is enough :)