iOS根据宽高寻找最合适的字体大小(二)

2,072 阅读5分钟

上一篇文章介绍了在宽高固定的情况下,寻找合适的字体大小使其可以单行完整显示的计算方式,经试验是可以满足要求的,但是如果对于长字符串,单行显示字体就变得很小,空间使用占比小,更好的方式应该是可以多行显示,尽量使文字填充整个空间。

对于这个需求,我在网上看到有一个网友给出的答案,先是定义一个字体,然后计算固定宽度情况下的高度,然后循环改变fontSize,使计算的高度搞好小于固定值,这个是一个思路,但是循环计算比较耗时,那有没有其他的思路呢?可惜在网上还没有看到其他的好方法。

本篇就来介绍一个本人实验的一种方法,这个方法经过了实验,是比较接近于最佳字体的方式,如果各位看了之后,有可改进之处,欢迎留言探讨。下面开始介绍该计算方法。

解题的思路和前一篇一样,寻找合适的字体比例因子,现在问题核心是如何确定这个比例因子。我的思路是,

1、确定M字体大小情况下,单个字符的宽高;
2、确定多少个字符可以刚好填充这个UILabel的空间;
3、确定字符串分Row行,Clum列;
4、求解除了单个字符的最佳宽和高;
5、和第1步计算出的宽高分别比较,取最小的那个值就是比例因子;
6、用M乘以该比例因子即是最合适的字体大小。

思路就是这样,下面是我的具体实施过程。

确定M字体大小情况下,单个字符的宽高

可以使用一个字符调用"国".boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil),获取到宽高,但是我在测试过程中发现,如果是英文或中英文混合时会出现比较大的偏差,所以我最后是采取整个字符串计算,然后使用宽度除以字符个数,取一个平均宽度值。如下:

let constraintRect = CGSize(width: CGFloat(MAXFLOAT), height: rowHeight())
let font = UIFont.init(name: info.fontName, size: 10) ?? UIFont.systemFont(ofSize: 10)
var boundingBox = danmuText.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)            
boundingBox.size.width = boundingBox.size.width / CGFloat(danmuText.count)
确定多少个字符可以刚好填充这个UILabel的空间

这个问题换个表述就是,将一个长方形进行切分,怎样切分得到的小长方形个数刚好大于等于M值,比如M=9,可以切分成3x3,也可以切分成1x9,2x5,3x4,这几个值,那该怎么选呢,我这里采用的方法是,对M进行开方,sqrt(M) ≈ value1,然后取value2 = floor(value1)和value3 = ceil(M / value2),这样得到value2 x value3是最接近M值得,可以认为value2行,value3列进行切分,那总可以填充的字符串就是value2 x value3个,代码如下:

//count表示字符总个数,取一个小,一个大是为了乘积结果可以刚好大于等于count
let count1 = CGFloat(count)
let value1 = sqrt(count1)
let value2 = floor(value1)
let value3 = ceil(count1/value2)
print("\(value1) , \(value2) , \(value3)")
确定字符串分Row行,Clum列

上面计算出来的结果值不一定满足实际情况,如果情况是宽高比例比较悬殊的情况下,合适的切分是1 x 9 而不是 3 x 3就需要考虑了,所以需要对上面计算出的结果值进行一次校对,那如何校对呢?我的办法是使其行列的比例接近于实际的宽高比,计算方式如下:

let rr = realSize.width / realSize.height
var row: CGFloat = 0.0
var clum: CGFloat = 0.0

 //公式: row * (rr * row) = value2 * value3
 row = sqrt(CGFloat(value2 * value3) / rr)
 clum = rr * row

上面得到的行列值,还需要进行校正,首先行列不能小于1,其次必须是整数,还是取一个floor,一个ceil,这样得到的值可能和value2 x value3的个数不吻合,大于没问题,小于则会导致字符显示不全,所以还需要根据宽高比例进行调整,如果宽大于高,那么就列大于等于行,如果高大于宽,那么久行大于等于列。代码如下:

//不能小于1
row = row > 1 ? row : 1
clum = clum > 1 ? clum : 1
        
//判断宽高比是不是更好,宽大于高,增加列,高大于宽,增加行
//宽大于高
if rr >= 1 {
        row = floor(row)
        clum = ceil(clum)
            
        if row * clum < value2 * value3 {
            let min = row > clum ? clum : row
            row = min
            clum = ceil(value2 * value3 / min)
        }
 } else {
        row = ceil(row)
        clum = floor(clum)
        if row * clum < value2 * value3 {
            let min = row > clum ? clum : row
            clum = min
            row = ceil(value2 * value3 / min)
        }
}
求解除了单个字符的最佳宽和高

上一步得到了row和clum值,这一步就简单了,用实际宽高进行计算即可,代码如下:

let width = realSize.width / CGFloat(clum)
let height = realSize.height / CGFloat(row)
        
let wordSize = CGSize(width: width, height: height)
和第1步计算出的宽高分别比较,取最小的那个值就是比例因子

这一步也比较简单,直接看代码,取最小的那个比例值:

let rate1 = wordSize.width / boundingBox.width
let rate2 = wordSize.height / boundingBox.height
var rate = rate1
if rate1 > rate2 {
       rate = rate2
}
用M乘以该比例因子即是最合适的字体大小

获取到了rate值,itemView.font = UIFont.systemFont(ofSize: size),重新设置UILabel的值或者attributedText即可。

下面是我实验的实际效果:


字体适配多行.gif
字体适配多行.gif

后记

前面已经说过,上面的方法得到的结果是比较接近于理想值,但是与理想值还是有一点差距的,主要是确定row和clum时会有偏差,比如M=5,在我这种宽高比例情况下得到的row=1,clum=6,而不是1 x 5,所以导致fontSize并不是最合适的,但是在考虑性能时,这是目前想到的比较理想的方法了,如果各位有新的方法,欢迎留言探讨。

原创文章,转载请告知作者,并标明出处和作者。