Я использую виртуализированный список ( response-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.
В моем случае я знаю следующее:
- Ширина контейнера
- Шрифт
- Шрифт size
- Все отступы
- Все поля
- Любые и все остальные стили, такие как line-height
Я ищу математическое решение, которое может вычислить высоту (или чрезвычайно близкую оценку), так что мне не нужно полагаться на какие-либо манипуляции с 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 - это самый простой подход, но я ' m готов приложить усилия к формуле c решения, даже если это означает каждый раз, когда я меняю поля, et c. Мне нужно убедиться, что входные данные функции обновлены.
Обновление: Это может помочь на пути к поиску ширины символа: Stati c карта ширины символа, откалиброванная через SVG ограничивающая рамка . Следующее содержит дополнительную информацию: Демо и подробности . Кредиты от go до Toph
Обновление 2: Благодаря использованию моноширинных шрифтов расчет ширины становится еще более упрощенным, поскольку вы только нужно измерить ширину одного символа. На удивление, в списке есть несколько очень хороших и популярных шрифтов, таких как Menlo и Monaco. update 1, я придумал что-то, что отлично работает для расчета количества строк. К сожалению, я видел, что в 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
(он не идеален, и я не уверен, почему).
Что касается интернационализации, похоже, он работал так же хорошо, как и с engli sh, за исключением того случая, когда я перешел на китайский язык. Я не знаю китайского, но, похоже, он следует другим правилам для перехода на новые строки, и это не учитывает эти правила.
К сожалению, как я уже сказал ранее, я заметил, что это не- по одному время от времени. Хотя это необычно, но не идеально. Я пытаюсь понять, что вызывает крошечные расхождения.
Тестовые данные, с которыми я работаю, генерируются случайным образом и составляют от 4 до 80 строк (и я генерирую 100 за раз).
Обновление 4: Я не Думаю, у меня больше нет отрицательных результатов. Изменение тонкое, но важное: вместо getNumberOfLines(text, width, styles)
вам нужно использовать getNumberOfLines(text, Math.floor(width), styles)
и убедиться, что Math.floor(width)
также является шириной, используемой в DOM. Браузеры несовместимы и по-разному обрабатывают десятичные пиксели. Если мы заставим ширину быть целым числом, то нам не о чем беспокоиться.