【发布时间】:2020-10-05 13:58:00
【问题描述】:
我正在利用一个虚拟化列表 (react-virtualized),其中需要我的列表项的高度并且可能会有很大差异。由于变化很大,我给图书馆的任何高度估计都会产生糟糕的体验。
通常的高度计算方法是这样的:
const containerStyle = {
display: "inline-block",
position: "absolute",
visibility: "hidden",
zIndex: -1,
};
export const measureText = (text) => {
const container = document.createElement("div");
container.style = containerStyle;
container.appendChild(text);
document.body.appendChild(container);
const height = container.clientHeight;
const width = container.clientWidth;
container.parentNode.removeChild(container);
return { height, width };
};
不幸的是,当您处理包含不同大小的项目的超大列表时,这并不高效。虽然可以利用缓存,但当您需要在一开始就知道总高度(所有项目的总高度)时,即使这样也不能很好地发挥作用。
通常使用的第二种解决方案是通过 HTML 画布'measureText。性能类似于上面的 DOM 操作。
就我而言,我知道以下几点:
- 容器宽度
- 字体
- 字体大小
- 所有填充
- 所有边距
- 任何和所有其他样式,例如行高
我正在寻找的是一个数学解决方案,它可以计算高度(或 非常 接近估计),这样我就不必依赖任何 DOM 操作,我可以得到随心所欲的高度。
我想它是这样的:
const measureText = (text, options) => {
const { width, font, fontSize, padding, margins, borders, lineHeight } = options;
// Assume this magical function exists
// This all depends on width, stying and font information
const numberOfLines = calculateLines(text, options);
const contentHeight = numberOfLines * lineHeight;
const borderHeight = borders.width * 2 // (this is all pseudo-code... but somehow get the pixel thickness.
const marginsHeight = margins.top + margins.bottom
const paddingHeight = padding.top + padding.bottom
return marginsHeight + paddingHeight + borderHeight + contentHeight;
}
在上面,我们缺少calculateLines 函数,这似乎是这项工作的首要任务。一个人将如何在这方面前进?我需要做一些预处理来确定字符宽度吗?既然我知道我正在使用的字体,这应该不是太大的问题,对吧?
是否存在浏览器问题?每个浏览器的计算方式可能会有什么不同?
还有其他需要考虑的参数吗?例如,如果用户有一些系统设置可以为他们放大文本(可访问性),浏览器会通过任何可用数据告诉我吗?
我知道渲染到 DOM 是最简单的方法,但我愿意将精力投入到公式化的解决方案中,即使这意味着每次我更改边距等时。我需要确保更新函数的输入.
更新:这可能有助于找到字符宽度:Static character width map calibrated via SVG bounding box。以下有更多信息:Demo and details。致谢Toph
更新2:通过monospaced typefaces的使用,宽度计算变得更加简单,只需要测量一个字符的宽度即可。令人惊讶的是,列表中有一些非常漂亮和流行的字体,例如 Menlo 和 Monaco。
大更新 3: 这是一个通宵,但通过更新 1 中的 SVG 方法的启发,我想出了一个计算行数的方法。不幸的是,我已经看到 1% 的时间它偏离了 1 行。大致代码如下:
const wordWidths = {} as { [word: string]: number };
const xmlsx = const xmlsn = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(xmlsn, "svg");
const text = document.createElementNS(xmlsn, "text");
const spaceText = document.createElementNS(xmlsn, "text");
svg.appendChild(text);
svg.appendChild(spaceText);
document.body.appendChild(svg);
// Convert style objects like { backgroundColor: "red" } to "background-color: red;" strings for HTML
const styleString = (object: any) => {
return Object.keys(object).reduce((prev, curr) => {
return `${(prev += curr
.split(/(?=[A-Z])/)
.join("-")
.toLowerCase())}:${object[curr]};`;
}, "");
};
const getWordWidth = (character: string, style: any) => {
const cachedWidth = wordWidths[character];
if (cachedWidth) return cachedWidth;
let width;
// edge case: a naked space (charCode 32) takes up no space, so we need
// to handle it differently. Wrap it between two letters, then subtract those
// two letters from the total width.
if (character === " ") {
const textNode = document.createTextNode("t t");
spaceText.appendChild(textNode);
spaceText.setAttribute("style", styleString(style));
width = spaceText.getBoundingClientRect().width;
width -= 2 * getWordWidth("t", style);
wordWidths[" "] = width;
spaceText.removeChild(textNode);
} else {
const textNode = document.createTextNode(character);
text.appendChild(textNode);
text.setAttribute("style", styleString(style));
width = text.getBoundingClientRect().width;
wordWidths[character] = width;
text.removeChild(textNode);
}
return width;
};
const getNumberOfLines = (text: string, maxWidth: number, style: any) => {
let numberOfLines = 1;
// In my use-case, I trim all white-space and don't allow multiple spaces in a row
// It also simplifies this logic. Though, for now this logic does not handle
// new-lines
const words = text.replace(/\s+/g, " ").trim().split(" ");
const spaceWidth = getWordWidth(" ", style);
let lineWidth = 0;
const wordsLength = words.length;
for (let i = 0; i < wordsLength; i++) {
const wordWidth = getWordWidth(words[i], style);
if (lineWidth + wordWidth > maxWidth) {
/**
* If the line has no other words (lineWidth === 0),
* then this word will overflow the line indefinitely.
* Browsers will not push the text to the next line. This is intuitive.
*
* Hence, we only move to the next line if this line already has
* a word (lineWidth !== 0)
*/
if (lineWidth !== 0) {
numberOfLines += 1;
}
lineWidth = wordWidth + spaceWidth;
continue;
}
lineWidth += wordWidth + spaceWidth;
}
return numberOfLines;
};
最初,我是逐个字符进行此操作,但由于字距调整以及它们如何影响字母组,逐字逐句更准确。同样重要的是要注意,尽管使用了样式,但必须在 maxWidth 参数中考虑填充。 CSS Padding 不会对 SVG 文本元素产生任何影响。它很好地处理了宽度调整样式letter-spacing(它并不完美,我不知道为什么)。
至于国际化,除了我进入中文的时候,它似乎和英语一样好用。我不懂中文,但它似乎遵循不同的规则来溢出到新行,这并没有考虑到这些规则。
不幸的是,就像我之前所说的,我注意到这是不时发生的。虽然这种情况不常见,但并不理想。我正在尝试找出导致微小差异的原因。
我正在使用的测试数据是随机生成的,长度为 4~80 行(我一次生成 100 行)。
更新 4: 我认为我不再有任何负面结果。更改很微妙但很重要:您需要使用getNumberOfLines(text, Math.floor(width), styles) 代替getNumberOfLines(text, width, styles),并确保Math.floor(width) 也是DOM 中使用的宽度。浏览器不一致并且以不同的方式处理十进制像素。如果我们强制宽度为整数,那么我们就不用担心了。
【问题讨论】:
-
我认为我从未见过不使用隐藏 DOM 元素的体面实现。即使那些通常仍然是“最好的猜测”并且并不完美。如果有人找到了,请分享。
-
@user120242 我也是。我目前正在摆弄我自己的宽度计算器。将报告结果。
-
@user120242 我编辑了更新。虽然从技术上讲它是在 DOM 上,但我必须说...... SVG 方法非常高效。甚至没有注意到一个信号,我正在处理一个大型数据集。
-
z̷̧̢̩̫̟͖̟͇͙̫̟͚̦̓͌̍̐̌̊̓ä̴̭̼̹̫͎͕͙͈͊̌̈̕̕͜ləgə是什么? span>
-
@Kaiido 我认为没有什么能很好地处理溢出问题 - 在 Chrome 上进行测试,它不会承认该文本或以任何方式适应它的高度。
标签: javascript html dom react-virtualized