Я использую виртуализированный список (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.
В моем случае я знаю следующее:
- Ширина контейнера
- Шрифт
- Размер шрифта
- Вся обивка
- Все поля
- Любые и все остальные стили, например 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 - это простейший подход, но я готов приложить усилия к шаблонному решению, даже если это означает, что каждый раз, когда я меняю поля и т. Д. Мне нужно обеспечить обновление входных данных функции.
Обновление: это может помочь на пути к поиску ширины символа: Карта ширины статического символа, откалиброванная с помощью ограничивающей рамки SVG < / а>. Ниже приведена дополнительная информация: Демо и подробности. Кредиты переходят на Toph
Обновление 2: благодаря использованию моноширинных шрифтов вычисление ширины становится еще более упрощенный, поскольку вам нужно измерить ширину только одного символа. Удивительно, но в списке есть несколько очень хороших и популярных шрифтов, таких как Menlo и Monaco.
Большое обновление 3: Это было довольно продолжительное время, но благодаря вдохновению с помощью метода SVG в обновлении 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
(он не идеален, и я не уверен, почему).
Что касается интернационализации, казалось, что она работает так же хорошо, как и с английским, за исключением того случая, когда я перешел на китайский. Я не знаю китайского, но, похоже, он следует другим правилам для перехода на новые строки, и это не учитывает эти правила.
К сожалению, как я уже сказал ранее, я заметил, что время от времени это происходит постепенно. Хотя это редко, но не идеально. Я пытаюсь понять, что вызывает крошечные несоответствия.
Тестовые данные, с которыми я работаю, генерируются случайным образом и составляют от 4 до 80 строк (а я генерирую 100 за раз).
Обновление 4: я больше не думаю, что у меня есть отрицательные результаты. Изменение тонкое, но важное: вместо getNumberOfLines(text, width, styles)
вам нужно использовать getNumberOfLines(text, Math.floor(width), styles)
и убедиться, что Math.floor(width)
- это ширина, также используемая в DOM. Браузеры несовместимы и по-разному обрабатывают десятичные пиксели. Если мы сделаем ширину целым числом, то нам не о чем беспокоиться.
letter-spacing
CSS, и мне нужно провести более обширное тестирование. Также ... Мне нужно выяснить, почему некоторые языки, такие как китайский, не работают. - person David   schedule 20.06.2020