文本自适应容器改变字体大小

4,241 阅读1分钟

对于单行文本,想要自适应容器改变字体大小比较简单,通过容器width和文本字数即可计算出字体大小
本文主要讲述多行文本自适应容器的方法(同样适用于单行的情况),多了一个维度,会稍微复杂一点

前言

  • 需要了解数学不等式的运算
  • 需要了解一元二次方程的求根公式
  • 本文不考虑字体间距以及行高,假定字体间距为0,行高为1,如需考虑,则在字体大小fs上加上相应的数值

预设变量

  • 待求解:字体大小 fs
  • 已知量:容器宽度 W、容器高度 H、文字字数 NUM
  • 未知量:当文本以最佳方式适应容器时,行数:row、列数:col

计算字体最大值最小值的步骤

  1. 根据行数、字体大小与容器高度之前的关系可以得到不等式:H - fs <= row * fs <= H

  2. 根据列数、字体大小与容器宽度之前的关系可以得到不等式:W - fs <= col * fs <= W

  3. 根据行高、列高、与文本字数之间的关系可得到不等式:row * col >= NUM

  4. 将三个不等式消除未知量(步骤1、2的 row * fs <= Hcol * fs <= W 相乘再结合步骤3抵消掉 rowcol)可以很容易得到:fs * fs * NUM <= H * W,解不等式可以得到 fs最大值,该值的极限情况是文本正好完全充满整个容器,实际上这种情况几乎是不存在的,因为很难保证每一行或者每一列正好容纳多少个字,会留有小于一个字大小的空隙。因此 fs 的最优值不能取这个最大值,会导致文本溢出容器

  5. 下面介绍如何获得 fs 的最小值。

    1. 利用步骤1、2的 H - fs <= row * fsW - fs <= col * fs ,相乘,可得到 (H - fs) * (W - fs) <= row * col * fs * fs
    2. 步骤3处理可得 fs * fs * NUM <= row * col * fs * fs
    3. 这两个不等式右边一致,我们比较左边,可以这么理解,一个面积为 H * W 的矩形,划分成一个个面积为 fs * fs 的正方形(实际肯定会留有缝隙,但是不影响这里作比较,假定可以正好划分),(H - fs) * (W - fs) 表示在原矩形基础上少了 R + L - 1(R表示一行,L表示一列)的正方形,而 fs * fs * NUM 表示有 NUM 个正方形,这个 NUM 遵循文本以最佳方式适应容器的原则,这么多个正方形必定会填充至容器的最后一行,也就是在原矩形基础上少了最多 R(R表示一行)正方形
    4. 因此,得到 (H - fs) * (W - fs) <= fs * fs * NUM,通过一元二次方程的求根公式可得 fs最小值
    5. 该值的极限情况是文本留有一行空隙填充容器,因此不会发生溢出,但是也无法满足以最佳方式适应容器(正好填充),fs最优解在最大最小值之间

推出最优解

  • 有两种方法,一种是从最大值出发,校验高度上是否溢出,溢出则减去固定值(设置为1即可)后再做校验,直到不溢出为止,得到的就是最优解
  • 另一种是从最小值出发,校验高度上是否溢出,不溢出则加1后再做校验,直到溢出为止,得到最优解
  • 因为分析可知最优解更靠近最小值,所以从最小值出发可减少循环次数
  • 校验方法:从最小值出发,Math.ceil(NUM / Math.floor(W / fs)) * fs > H 满足该条件则溢出了,中止循环,否则 fs + 1 继续循环,循环结束后 fs - 1 为最优解
  • 得到最优解后可以设置期望字体大小的最大值和最小值

参考代码

autoModifyFontSize(elList){
    elList.forEach(el => {
        const NUM = el.innerText.length;
        const W = el.offsetWidth;
        const H = el.offsetHeight;
        const minFontSize = 28;
        const maxFontSize = 84;
        // 边界最大最小值, 三个不等式 H - fs <= row * fs <= H    W - fs <= column * fs <= W     row * column >= NUM(当成等于处理)
        // let fontSizeMax = Math.sqrt(W * H / NUM);
        // console.log('Max', fontSizeMax)
        let fontSizeMin = (Math.sqrt((W + H) * (W + H) + 4 * W * H * NUM - 4 * W * H) - W - H) / (2 * NUM - 2)
        console.log('Min', fontSizeMin)

        let signState = true;
        let fontSize = fontSizeMin;
        while (signState) {
            if(Math.ceil(NUM / Math.floor(W / fontSize)) * fontSize > H){
                // 中止
                signState = false;
                break;
            }else{
                // 继续
                signState = true;
            }
            fontSize = fontSize + 1;
        }
        fontSize = fontSize - 1;

        if(fontSize >= maxFontSize){
            fontSize = maxFontSize
        }
        if(fontSize <= minFontSize){
            fontSize = minFontSize
        }
        console.log(fontSize)
        el.style.fontSize = `${fontSize}px`;
    })
},