关于javascript:JavaScript实现文本溢出自动缩小字体到N行适应容器

37次阅读

共计 6559 个字符,预计需要花费 17 分钟才能阅读完成。

当一个页面须要切换不同的语言时,同一句文案,不同语言的文本长度很难保持一致,有的时候,文本无限度换行显示会影响整个页面布局,因而须要应用一些办法来确保文本容器在不同的语言中都放弃布局不变形,也就是将文本限度在 N 行以内显示,保障不把容器撑开也不溢出容器的同时,尽可能在无限的空间里显示更多文本。

(设计稿给出文本最多一行)

(理论页面在英文文案下变成了两行)
如何将文本限度在 N 行显示,通常会用以下几种办法:

  1. 文本超出 N 行显示滚动条
  2. 文本超出 N 行显示省略号
  3. 文本超出 N 即将字号设置为某个更小的字号,如果还是超出则显示省略号
  4. 文本超出 N 即将字号放大至刚好铺满容器,最小放大到某个字号,如果还是超出则显示省略号

第 1、2 种办法通过设置 html 和 css 就能够办到了,比较简单,文章次要探讨第 3、4 种办法的可行性以及实现过程。

为了不便操作,先将文本套上一个 span 标签再放到容器 (container) 中,比方<div><span>text</span></div>,并让容器的高度始终小于等于文本内容的高度或由文本内容撑开,而后咱们就能够对容器进行字号的设置操作。

文本溢出判断

不论是文本超过 N 行设置固定字号还是主动调整字号,都须要先判断精确文本是否超出,判断溢出的办法有很多,在这里咱们通过拿到文本以后高度与 N 行文本容许的最大高度进行比拟。文本的以后高度能够通过 container.scrollHeight 取得,N 行文本容许的最大高度则能够通过文本的行高 lineHeight * N 取得(单行文本的高度是由文本的行高决定的),最初比拟两者大小得出文本溢出状况。

/**
 * @param {Number} lineNum 最大容许的文本行数
 * @param {html element} containerEle 文本容器
 * @returns Boolean 返回文本是否溢出最大容许的文本函数
 */
function isTextOverflow(lineNum, containerEle) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {throw new Error('container\'s lineHeight must be a exact value!');
  }

  const lineHeight = Number(computedStyle.lineHeight.slice(0, -2));

  if (containerEle.scrollHeight > Math.ceil(lineHeight * lineNum)) {isOverflow = true;}
  return isOverflow;
}

首先通过 getComputedStyle 办法拿到 scrollHeightlineHeight,留神应用这种办法须要容器的 lineHeight 为明确的值,如果容器的 lineHeight 被省略或者设置为关键字,就无奈获取具体的数值,最初再将两者的比拟后果返回即可。

N 行文本放大字号适应容器

如果问题变成单行文本放大字号适应容器,就变得简略得多了,咱们只须要调整字号使得文本内容的宽度和容器的宽度刚好相等即可,也就是须要拿到以后文本字号fontSize,以后文本未折行时的宽度textWidth,以后容器宽度containerWidth,指标字号 = fontSize * containerWidth / textWidth

要获取文本未折行时的宽度,最简略也最容易想到的方法就是先将容器的 whiteSpace 设置为 nowrap,期待浏览器重排重绘后获取未折行的宽度,再将whiteSpace 重制,或者创立一个额定的 whiteSpacenowrap的复制元素插入 dom 中,期待浏览器重排重绘后获取。通过这个形式咱们能够很容易写出单行文本主动放大字号适应容器的办法。

/**
 * @param {*} containerEle 文本容器
 * @param {*} minFontSize 限度最小能够放大到的字号
 * @returns 
 */
 function adjustFontSizeSingle(containerEle, minFontSize = 8) {return new Promise(async (resolve) => {if (!isTextOverflow(1, containerEle)) {resolve();
      return;
    }

    const computedStyle = getComputedStyle(containerEle);
  
    const needResetWhiteSpace = computedStyle.whiteSpace;
    const needResetOverflow = computedStyle.overflow;
    const fontSize = Number(computedStyle.fontSize.slice(0, -2));
  
    // 设置文本不折行以计算文本总长度
    containerEle.style.whiteSpace = 'nowrap';
    containerEle.style.overflow = 'hidden';
  
    await nextTick();
  
    const textBody = containerEle.childNodes[0];
    if (containerEle.offsetWidth < textBody.offsetWidth) {
      // 按比例放大字号到刚好占满容器
      containerEle.style.fontSize = `${Math.max(fontSize * (containerEle.offsetWidth / textBody.offsetWidth),
        minFontSize
      )}px`;
    }
  
    containerEle.style.whiteSpace = needResetWhiteSpace;
    containerEle.style.overflow = needResetOverflow;
    resolve();});
}

await adjustFontSizeSingle(ele);
console.log('调整实现');


调整前

调整后

演示》》

用下面的办法须要进行一次额定的 dom 操作,那么能不能省下这一次 dom 操作呢,咱们的能够用 CanvasRenderingContext2D.measureText()来计算文本未折行时的宽度。通过给 canvas 的画笔设置雷同的字号与字体,再调用measureText,即可失去与原生 dom 雷同的单行字符串宽度。

/**
 * @param {*} containerEle 文本容器
 * @param {*} minFontSize 限度最小能够放大到的字号
 * @param {*} adjustLineHeight 是否按比例调整行高
 * @returns 
 */
async function adjustFontSizeSingle(containerEle, minFontSize = 16, adjustLineHeight = false) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {throw new Error('container\'s lineHeight must be a exact value!');
  }

  let lineHeight = Number(computedStyle.lineHeight.slice(0, -2));
  let fontSize = Number(computedStyle.fontSize.slice(0, -2));

  if (isTextOverflow(1, containerEle)) {isOverflow = true;}

  if (!isOverflow) {return;}

  const textBody = containerEle.childNodes[0];

  if (!offCtx) {offCtx = document.createElement('canvas').getContext('2d');
  }
  const {fontFamily} = computedStyle;
  offCtx.font = `${fontSize}px ${fontFamily}`;
  const {width: measuredWidth} = offCtx.measureText(textBody.innerText);

  if (containerEle.offsetWidth >= measuredWidth) {return;}

  let firstTransFontSize = fontSize;
  let firstTransLineHeight = lineHeight;
  firstTransFontSize = Math.max(
    minFontSize,
    fontSize * (containerEle.offsetWidth / measuredWidth)
  );
  firstTransLineHeight = firstTransFontSize / fontSize * lineHeight;

  fontSize = firstTransFontSize;
  containerEle.style.fontSize = `${fontSize}px`;
  if (adjustLineHeight) {
    lineHeight = firstTransLineHeight;
    containerEle.style.lineHeight = `${lineHeight}px`;
  }
  console.log('溢出调整实现');
}

演示》》

当问题回升到多行文本时,或者咱们能够仿造单行文本那样通过计算(容器宽度 * 行数)与(文本未折行的总宽度)的比来失去文本须要放大的比例,但却没有这么简略,因为文本的换行并不是简略的将字符串等分切凋谢在每一行,而会遵循排版换行的规定,具体规定参考 Unicode 换行算法 (Unicode Line Breaking Algorithm, UAX #14)

Unicode 换行算法形容了这样的算法:给定输出文本,该算法将产生被称为换行机会(break opportunities)的一组地位,换行机会指的是在文本渲染的过程中容许于此处换行,不过理论换行地位须要联合显示窗口宽度和字体大小由更高层的应用软件另行确认。

也就是说文本并不是在每一个字符都能够换行的,它会在通过算法失去的有最近换行机会的字符换行,浏览器当然也恪守了这个规定,此外也能通过 css3 来自定义一些换行规定

  • line-break:用来解决如何断开带有标点符号的中文、日文或韩文(CJK)文本的行
  • word-break:指定怎么在单词内断行
  • hyphens:告知浏览器在换行时如何应用连字符连贯单词
  • overflow-wrap:用来阐明当一个不能被离开的字符串太长而不能填充其包裹盒时,为避免其溢出,浏览器是否容许这样的单词中断换行。

好在咱们并不需要齐全精准地计算刚好能占满容器的字号,能够先通过计算(容器宽度 * 行数)与(文本未折行的总宽度)的比初步失去文本须要放大到的字号,受害于换行规定,此时文本仍然可能有溢出的状况产生,但离刚好占满容器须要放大到的字号间隔很近了,通过无限的几次循环放大字号即可在肯定误差范畴内失去咱们想要的字号。

async function adjustFontSizeLoop(lineNum, containerEle, step = 1, minFontSize = 8, adjustLineHeight = false) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {throw new Error('container\'s lineHeight must be a exact value!');
  }

  let lineHeight = Number(computedStyle.lineHeight.slice(0, -2));
  let fontSize = Number(computedStyle.fontSize.slice(0, -2));
  if (containerEle.scrollHeight <= Math.ceil(lineHeight * lineNum)) {return;}

  const textBody = containerEle.childNodes[0];

  if (!offCtx) {offCtx = document.createElement('canvas').getContext('2d');
  }
  const {fontFamily} = computedStyle;
  offCtx.font = `${fontSize}px ${fontFamily}`;
  const {width: measuredWidth} = offCtx.measureText(textBody.innerText);
  if (containerEle.offsetWidth * lineNum >= measuredWidth) {return;}

  let firstTransFontSize = fontSize;
  let firstTransLineHeight = lineHeight;
  firstTransFontSize = Math.max(
    minFontSize,
    fontSize * (containerEle.offsetWidth * lineNum / measuredWidth)
  );
  firstTransLineHeight = firstTransFontSize / fontSize * lineHeight;

  fontSize = firstTransFontSize;
  containerEle.style.fontSize = `${fontSize}px`;
  if (adjustLineHeight) {
    lineHeight = firstTransLineHeight;
    containerEle.style.lineHeight = `${lineHeight}px`;
  }

  if (lineNum === 1) {return;}
  
  let runTime = 0;
  do {await nextTick();
    if (containerEle.scrollHeight > Math.ceil(lineHeight * lineNum)) {isOverflow = true;} else {isOverflow = false;}
    if (!isOverflow) {break;}
    runTime += 1;
    const transFontSize = Math.max(fontSize - step, minFontSize);
    if (adjustLineHeight) {lineHeight = this.toFixed(transFontSize / fontSize * lineHeight, 4);
      containerEle.style.lineHeight = `${lineHeight}px`;
    }
    fontSize = transFontSize;
    containerEle.style.fontSize = `${fontSize}px`;
  } while (isOverflow && fontSize > minFontSize);
  console.log('溢出调整实现, 循环设置字号:', runTime, '次');
}

演示》》

调整前

调整为限度 2 行显示

下一步?

当初曾经能够不那么完满的解决这个问题了(有不确定循环次数的代码总是不太能让人心安),而且看起来效率和成果也过得去。有没有能一次就算出最终须要放大的字号呢?

咱们能够先尝试失去一段文本在何处换行的信息,当然本人是没有这么多精力去手动实现换行规定,然而咱们能够站在伟人的肩膀上,用他人实现好的开源库:niklasvh/css-line-break。


失去换行信息之后该怎么计算呢?让我再思考一会。

正文完
 0