Zong
低代码网格布局

时隔一年,又重新参与低代码的建设,重新根据业务形态抽象网格布局逻辑。

那就来分享一下网格布局的实现逻辑。

网格布局

基础信息

设计将网格横向分为 12 列,并根据实际页面宽度(或者页面预设固定宽度)计算每列实际列宽,以作为 w 的单位映射值,即 w 最大为 12

行高固定为 32px ,即 h 的单位映射值,不限制最大值。

格间间距为 16px 。

组件设计

GridItem 网格项

此组件会体现在 GridLayout 下,会对业务组件或基础组件等进行包裹,以保证 GridLayout 在渲染时,对 children 各组件进行包裹。

并且,此组件自身无 Schema 配置信息,仅从被包裹 Schema 中获取 size 以呈现组件具体大小(实际大小),或者映射大小(网格单位,通过计算呈现实际大小)。

GridLayout 网格容器

此组件为特殊内置组件(与业务组件或基础组件一致),具备 Schema 配置信息。

属性 备注 类型
layouts children 中组件的位置信息 Array<LayoutItem>
相关类型
// 设计参考 react-grid-layout 配置
interface LayoutItem {
  id: string
  x: number
  y: number
  w: number
  h: number
  gap: Record<'top' | 'right' | 'bottom' | 'left', number>
}

网格交互

网格交互设计分为:插入、移动、大小调整、删除

插入

获取环境信息

  • 是否选中组件
  • LayoutItem 默认 w 为 6 , h 为 6
点击插入
  1. 若未选中组件,则默认插入根 GridLayout
  2. 更新 GridLayoutlayoutsLayoutItem 数据至尾部(生成默认 wh
  3. 布局计算
  4. 若选中组件,获得选中组件信息
选中 GridLayout
  1. 更新 GridLayoutlayoutsLayoutItem 数据至尾部(生成默认 wh
  2. 布局计算
选中业务组件或基础组件
  1. 获得选中组件 parent
  2. 若是 GridLayout ,则更新 GridLayoutlayoutsLayoutItem 数据至选中组件之后(生成默认 wh
  3. 布局计算
  4. 若不是则默认插入,不执行网格布局逻辑
选中容器组件

容器组件为特殊组件,即业务组件或基础组件支持 children 渲染

  1. 默认插入,不执行网格布局逻辑
移动插入
  1. 获得鼠标落点(鼠标抬起位置)
  2. 获得落点组件信息
  3. 逻辑同「点击插入」时选中组件情况

移动

  1. 获得鼠标落点(鼠标抬起位置)
  2. 获得落点组件信息
落点 GridLayout
  1. 检查落点 GridLayout 是否与当前移动组件的 parent 一致
  2. 若一致则更新对应 layoutsLayoutItem 数据至尾部
  3. 布局计算
  4. 若不一致则执行删除和插入(采用当前 wh )操作
落点业务组件或基础组件
  1. 获得落点组件 parent 是否与当前移动组件的 parent 一致
  2. 若一致则交换对应 layoutsLayoutItem 数据(除了交换,也可以是插入,具体根据自身业务决定)
  3. 布局计算
  4. 若不一致则执行删除和插入(采用当前 wh )操作
落点容器组件

容器组件为特殊组件,即业务组件或基础组件支持 children 渲染

  1. 执行删除和插入(不执行网格布局逻辑)操作
  2. 布局计算

大小调整

  1. 获得鼠标 xy 的偏移量(鼠标按下到鼠标抬起后获得)
  2. 根据当前组件 Schema 大小累加偏移量(四舍五入)计算组件新大小,即 wh 的值
  3. 更新对应 layoutsLayoutItem 数据
  4. 布局计算

删除

  1. 移除组件 Schema
  2. 移除对应 layoutsLayoutItem (若存在)
  3. 布局计算(若存在)

布局计算

  1. 获得 layouts 信息
  2. 依次基于 wh 的值计算 xy 位置
  3. 并且根据当前 whxy 的关系计算格间间距值
  4. 更新 layouts 信息(格间间距在渲染、位置阴影、选中框时进行累加,不影响原值)
  5. 渲染更新

算法根据自身业务决定,可以使用二维数组进行占位计算,或是使用自然堆叠计算方式,或是其他。

算法 A 算法 B
algorithm-a algorithm-b

算法 A

依次遍历 layouts 以返回新的 xy 位置

  1. 初始化锚点 xy 位置 (0, 0) ,行内标和行标 (0, 0) ,缓存最大 y 为 0
  2. 依次遍历,记录前一次的缓存最大 y 位置(用于计算格间间距)
  3. 比较 LayoutItemxw 的和是否大于 12
  4. 若小于等于,则更新当前 LayoutItemxy 位置
  5. 同时更新锚点 x 的位置为当前 LayoutItemxw 的和
  6. 根据当前 LayoutItemyh 的和,缓存最大 y 位置(取最大值)
  7. 若大于,则表示溢出,重置锚点 x 为 0 ,将锚点 y 设置为缓存最大 y
  8. 更新当前 LayoutItemxy 位置
  9. 重复 5, 6
  10. 每计算出 LayoutItemxy 位置后,比较前一次的缓存最大 y 位置和缓存最大 y 位置
  11. 若前一次的缓存最大 y 位置小于缓存最大 y 位置(说明换行过),增加行标,重置行内标为 0
  12. 反之则增加行内标
  13. 将当前行内标和行标与 LayoutItem 对应关联

至此,已实现更新 xy 位置,接下来结合行内标和行标信息来计算 LayoutItem 的格间间距。

倒序遍历行内标和行标

  1. 设置第一次右间距存在标识为否,前一次行标为行内标和行标信息的最后一个值
  2. 倒序遍历,获得对应的行内标和行标
  3. 初始化 LayoutItem 对应的格间间距,左右上为 0 ,下为 1/2 间距 8px
  4. 若前一次行标与当前行标不一致(意味着行变更),则重置第一次右间距存在标识为否
  5. 更新前一次行标为当前行标
  6. 若当前行标大于 1 ,则表示非第一行,设置格间间距上为 8px
  7. 若第一次右间距存在标识为是,或者当前对应的 LayoutItemxw 的和小于 12 ,则设置格间间距右为 8px
  8. 若当前行内标大于 0 ,则设置格间间距左为 8px ,并将第一次右间距存在标识设置为是
  9. 更新格间间距至对应的 LayoutItem

算法 B

依次遍历 layouts 以返回新的 xy 位置

  1. 初始化空二维数组(记做标记空间),用于标记占位情况;y 标识用于记录需要延展的行数,初始为 0 ,以及上一次的 y 标识,同初始为 0
  2. 依次遍历,比较上一次的 y 标识与 y 标识加 LayoutItemh ,取最大值更新上一次的 y 标识
  3. 使用上一次的 y 标识与标记空间长度比较,若大于 0 ,则批量生成行 new Array(12).fill(null)
  4. (0, 0) 标记位进行遍历(可优化至最左上角的一个 null 标记位),寻找 null 标记位
  5. 当找到 null 标记位后,以此标记位为起点,检查 LayoutItemwh 占位情况(以标记位为起点向右下检查 LayoutItemwh 长度,不能越界)
  6. 若均为 null 则更新标记空间,并更新 LayoutItemxy 为当前标记位起点,同时更新 y 标识为当前标记位起点 yLayoutItemh 的和,中断此次标记位遍历,进入下一个 LayoutItem 遍历
  7. 反之寻找下一个 null 标记位起点

至此,已实现更新 xy 位置,接下来结合标记空间来计算 LayoutItem 的格间间距。

  1. 遍历标记空间 4 条边,记录占用 LayoutItem 信息
  2. 依次遍历 LayoutItem 并初始化对应的格间间距,左右上下为 8px
  3. 检查占用信息,对 4 条边存在占用的方向格间间距设置为 0
  4. 更新格间间距至对应的 LayoutItem

辅助功能

  1. 组件选中框跟随组件一起移动
  2. 计算组件预计落点位置阴影
  3. 动态替换