预判 LazyVGrid 布局 - Flexible篇

3,597 阅读6分钟

写在开始

此篇文章假定读者已经知道LazyVGrid+LazyHGrid的基本用法,以LazyVGrid为例,在开始之前在明确下Grid渲染列的几种类型:

  1. fixed: 列会拥有固定的宽度,空间的大小不会对其宽度造成影响,显示一列
  2. flexible: 可以指定最小值和最大值,空间的大小会对其造成影响,但是最终显示的宽度一定是在范围内,不会无限缩小或者放大,显示一列
  3. adaptive: 可以指定最小值和最大值,空间的大小会对其造成影响,显示尽可能多的列

注:不设置间距-默认间距为8

直接举栗子

fixed

[GridItem(.fixed(60))]

image.png

结论:不在意容器宽度,使用固定宽度, 渲染宽度为60

fixed+fixed

[
GridItem(.fixed(60)),
GridItem(.fixed(100))
]

image.png

结论:不在意容器宽度,使用固定宽度, 渲染宽度为60 + 8 + 100

flexible

[GridItem(.flexible(minimum: 100, maximum: 200))]

image.pngimage.png

结论:根据容器宽度变化, 渲染宽度为max(min(容器宽度, 200), 100)

flexible+flexible

GridItem(.flexible(minimum: 100)),
GridItem(.flexible(minimum: 180))

image.png

事情开始变得有趣起来了,你可能刚开始猜的效果,应该是100+8+180,但是显示结果却完全不同。接下来让我们通过计算来看看这个布局过程。

1、建议宽度为 300, 减去间距后宽度为 300 - 8 = 292 所以每一列的建议宽度为 292 / 2 = 146, 比对第一列,最小值为100,所以第一列直接使用146作为建议宽度,剩余宽度为146, 比对第二列,最小值为180,选择 180 作为建议宽度, 最终的计算宽度为 146 + 8 + 180 = 334

2、开始渲染,(334 - 8) / 2 = 163, 第一列够了,渲染宽度为163,第二列不够,直接使用180,所以最终渲染效果为 163 + 8 + 180

注:显示不居中,第一步结束此时居中效果为 (326 + 8 -300) / 2 = 17, 第二不绘制时多余 351 - 300 - 17 = 34,所以最终显示效果为 左侧超出 17, 右侧超出 34


然后你信心满满以为自己已经天下无敌了,但是现实狠狠的给你了一个大逼兜子。还是[Flexible+Flexible]

GridItem(.flexible(minimum: 180)),
GridItem(.flexible(minimum: 100))

image.png

别怀疑人生,其实还是原来的配方,还是原来的味道,让我们开始

1、建议宽度为 300, 减去间距后宽度为 300-8=292, 所以每一列的建议宽度为 292/ 2 = 146, 比对第一列,最小值为180,所以第一列直接占用 180,剩余宽度为 292 - 180 = 112,然后比对第二列,最小值为100,所以第二列可以占用112,最终计算结果为 180 + 8 + 112 = 300

2、以300开始渲染,步骤同1


flexible + fixed + flexible

GridItem(.flexible(minimum: 180)),
GridItem(.fixed(minimum: 100)),
GridItem(.flexible(minimum: 100))

image.png 1、起始宽度300, 减去固定宽度和间距之后为300 - 100 - 8 - 8 = 184, 减去固定列之后,剩余两列, 所以第一列的建议宽度为 184 / 2 = 92, 比对第一列,最小值为 180,所以第一列直接占用 180,剩余宽度为 184 - 180 = 4, 比对第二列,最小值100, 所以第二列占用 100,最终计算结果为 180 + 8 + 100 + 8 + 100 = 396

2、渲染以396开始,减去固定宽度和间距之后为396 - 100 - 8 - 8 = 280,减去固定列之后,剩余两列,所以第一列的渲染宽度为280 / 2 = 140, 比对第一列,最小值为 180, 所以第一列占用 180,剩余宽度为 280 - 180 = 100, 比对第二列,最小值为 100,所以 第二列占用 100,进行渲染,渲染宽度为 180 + 8 + 100 + 8 + 100 = 396

注: 渲染器起始和最终渲染宽度一致,显示效果为居中显示


GridItem(.flexible(minimum: 100)),
GridItem(.fixed(minimum: 100)),
GridItem(.flexible(minimum: 180))

image.png

1、起始宽度为300,减去固定宽度和间距之后为300 - 100 - 8 - 8 = 184, 减去固定列之后,剩余两列,所以第一列的建议宽度为 184 / 2 = 92, 比对第一列,最小值100,所以使用100,剩余宽度为184 - 100 = 84,比对第二列,最小值 180, 所以第二列占用 180,最终计算结果为 100 + 8 + 100 + 8 + 180 = 396

2、渲染以396开始,减去固定宽度和间距之后为396 - 100 - 8 - 8 = 280,减去固定列之后,剩余两列,以第一列的渲染宽度为280 / 2 = 140, 比对第一列,最小值为 100, 所以第一列占用 140, 剩余宽度为 280 - 140 = 140,比对第二列,最小值为180, 所以第二列占用 180, 最终渲染宽度为 140 + 8 + 100 + 8 + 180 = 436

注:起始396作为起始计算渲染,起始居中 布局为 48 - 300 - 48, 起始位置固定, 渲染宽度为436,最终渲染结果为 48 - 300 - 88

结论 - 渲染宽度 = f(f(建议宽度))

步骤一:计算

1、从 建议宽度 开始

2、减去固定宽度列的宽度,以及列之间的间距,计算出剩余宽度
    
3、遍历每列:
flexible: 根据剩余宽度和剩余列数的平均值,以及当前列的限制,确定列的宽度,之后 从剩余宽度中减去列的宽度
fixed: 直接取值
adaptive: 下一章 - 待定
    
4、最终得到渲染的起始宽度

步骤二:渲染

1、从 步骤一的 结果开始

2 - 4 重复步骤一

5、得到最终的渲染结果    

按照结论步骤,简单写一下这个过程的计算代码

extension View {

    func draw(_ items: [GridItem], contentWidth: CGFloat) -> [CGFloat] {
        let result1 = calculate(items, contentWidth: contentWidth).reduce(into: 0) { $0 += $1 }
        let spacings = items.dropLast().reduce(into: 0) { $0 += ($1.spacing ?? 8) }

        return calculate(items, contentWidth: result1 + spacings)
    }

    

    func calculate(_ items: [GridItem], contentWidth: CGFloat) -> [CGFloat] {
        var result = Array<CGFloat>(repeating: 0, count: items.count)

        var dynamicWidth = contentWidth
        
        var dynamicCount = items.count

        /// 减去固定宽度列的宽度,以及列之间的间距
        for (index, item) in items.enumerated() {
            /// 最后一行间距不计算
            if index != items.count - 1 {
                dynamicWidth -= (item.spacing ?? 8)
            }

            if case .fixed(let width) = item.size {
                dynamicCount -= 1
                dynamicWidth -= width
            }

        }

        if dynamicCount > 0 {
            /// 遍历所有列
            for (index, item) in items.enumerated() {
                switch item.size {
                case let .flexible(minimum, maximum):
                    let singleWidth = dynamicWidth / CGFloat(dynamicCount)
                    let width = min(maximum, max(minimum, singleWidth))
                    result[index] = width

                    dynamicCount -= 1
                    dynamicWidth -= width
                case .fixed(let width):
                    result[index] = width
                case .adaptive:
                    /// 此处下一章补全
                    continue
                @unknown default:
                    continue
                }
            }
        }
        return result
    }
}

配套Demo

Adaptive篇