Как рассчитать высоту текста без рендеринга в DOM? - PullRequest
19 голосов
/ 16 июня 2020

Я использую виртуализированный список ( 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. Браузеры несовместимы и по-разному обрабатывают десятичные пиксели. Если мы заставим ширину быть целым числом, то нам не о чем беспокоиться.

Ответы [ 2 ]

1 голос
/ 24 июня 2020

ИМХО, суть этого вопроса заключается в следующих нескольких словах:

К сожалению, когда вы имеете дело с очень большими списками с элементами разного размера, это неэффективно. Хотя кеш можно использовать, даже это не работает так хорошо, когда вам нужно знать общую высоту (высоту всех элементов вместе) в самом начале.

Это сильно контрастирует с JavaScript природой и философией: объединение «очень больших списков» и «в самом начале» - это то, что не работает в JavaScript.

Вероятно, вы сможете добиться лучших результатов с меньшими усилиями, если вы сосредотачиваетесь на том, что заставляет вас сказать «в самом начале», вместо того, чтобы искать действительный ответ на этот вопрос. Независимо от того, насколько производительным является решение, которое вы можете найти, когда «чрезвычайно большие списки» продолжают расти, ваше решение неизбежно вызовет блокировку пользовательского интерфейса.

Это всего лишь мои два цента.

1 голос
/ 19 июня 2020

Я нашел Алгоритм измерения текста , который приближает ширину строк, не касаясь DOM.

Я немного изменил его, чтобы вычислить количество строк (где вы застряли) .

Вы можете рассчитать количество строк, как показано ниже:

/**
 * @param text : <string> - The text to be rendered.
 * @param containerWidth : <number> - Width of the container where dom will be rendered. 
 * @param fontSize : <number> - Font size of DOM text
**/

function calculateLines(text, containerWidth, fontSize = 14) {
  let lines = 1;  // Initiating number of lines with 1

// widths & avg value based on `Helvetica` font.
  const widths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.278125,0.278125,0.35625,0.55625,0.55625,0.890625,0.6671875,0.1921875,0.334375,0.334375,0.390625,0.584375,0.278125,0.334375,0.278125,0.303125,0.55625,0.55625,0.55625,0.55625,0.55625,0.55625,0.55625,0.55625,0.55625,0.55625,0.278125,0.278125,0.5859375,0.584375,0.5859375,0.55625,1.015625,0.6671875,0.6671875,0.7234375,0.7234375,0.6671875,0.6109375,0.778125,0.7234375,0.278125,0.5,0.6671875,0.55625,0.834375,0.7234375,0.778125,0.6671875,0.778125,0.7234375,0.6671875,0.6109375,0.7234375,0.6671875,0.9453125,0.6671875,0.6671875,0.6109375,0.278125,0.35625,0.278125,0.478125,0.55625,0.334375,0.55625,0.55625,0.5,0.55625,0.55625,0.278125,0.55625,0.55625,0.2234375,0.2421875,0.5,0.2234375,0.834375,0.55625,0.55625,0.55625,0.55625,0.334375,0.5,0.278125,0.55625,0.5,0.7234375,0.5,0.5,0.5,0.35625,0.2609375,0.3546875,0.590625]
  const avg = 0.5293256578947368

  text.split('')
    .map(c => c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg)
    .reduce((cur, acc) => {
      if((acc + cur) * fontSize  > containerWidth) {
          lines ++;
          cur = acc;
      }
      return acc + cur;
    }); 

  return lines;
}

Примечание

Я использовал Helvetica как font-family, вы можете получить значение widths & avg из Измерьте текст в соответствии с font-family, которое у вас есть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...