功能概览
低代码设计中最重要的一种布局方式就是栅格布局,将物料拖拽至设计器中,会根据其设定的span之类的值自适应填充位置,与之相关的场景比如表单设计、excel打印设计等
- 表单设计
- excel打印设计
方案选择
flex
下图为antd的栅格方案:
flex弹性布局是一个很不错的方案,简单、上手快,只要按照antd的Row、Col那样实现一个布局栅格系统系统,根据节点的栅格大小去动态添加对应的class,似乎就能cover住咱们的功能,但是想要满足excel打印设计那种,有行合并的需求的话,就只能含泪跟flex说拜拜了 orz。
grid
在寻找解决方案的途中,偶然看到了一个网站 CSS Grid Generator ,你可以按照自己的需求动态去生成想要的grid样式配置,比如要实现上图的excel布局
效果图
代码
.parent {
/*指定一个容器采用网格布局 */
display: grid;
/*每一列的列宽 repeat相当一个遍历,减少代码 24列,每列1/24 */
grid-template-columns: repeat(24, 1fr);
/*每一行的行高 repeat同上 */
grid-template-rows: repeat(3, 1fr);
/*置列与列的间隔*/
grid-column-gap: 0px;
/*置行与行的间隔*/
grid-row-gap: 0px;
}
/*实现核心 grid-area属性指定项目放在哪一个区域*/
/*grid-area: <row-start> / <column-start> / <row-end> / <column-end>*/
/*row-start行开始的位置 column-start列开始的位置*/
/*row-end行结束的位置 column-end列结束的位置*/
.div1 { grid-area: 1 / 1 / 4 / 5; }
.div2 { grid-area: 1 / 5 / 2 / 25; }
.div3 { grid-area: 2 / 5 / 4 / 15; }
.div4 { grid-area: 2 / 15 / 3 / 25; }
.div5 { grid-area: 3 / 15 / 4 / 25; }
Ops!这不就是瞌睡遇上枕头,冥冥中就感觉他一定能解决我的需求,想要为我所用,于是乎简单去了解下相关的文档,相关注释见上面代码
API设计
根据grid的属性,我们需要两个栅格布局组件,一个是Container去负责计算出children正确的grid-area,GridItem就根据生成的grid-area去渲染出我们节点,伪代码如下。
function () {
return (
<Container columns={DEFAULT_SYSTEM_COL}>
{ schema.map((node) => {
const {id, col, row} = node.props;
return (
<GridItem
key={id}
col={col}
row={row}
>
<Node />
</GridItem>
)
})}
)
}
</Container>
代码实现
interface IContainer {
columns: number;
gap?: number;
rows?: number;
}
interface IGridItem {
row: number;
col: number;
border?: boolean;
// 这里的full需要解释下:由于我们的思路是自适应铺满,
// 但是有的时候就是需要一个元素占一整行,但又不铺满的效果,
// 此刻就需要用full去标记
full?: boolean;
gridArea?: string;
}
const GridItem: React.FC<IGridItem> = props => {
return (
<div
className={cns('brick-grid-system-item', { border: props.border })}
style={{ gridArea: props.gridArea }}
>
{props.children}
</div>
);
};
const Container: React.FC<IContainer> & {
GridItem: typeof GridItem;
} = props => {
const { columns, gap, rows, children } = props;
const renderChildren = () => {
// how to calc child's gridArea
};
const containerStyle = useMemo(() => {
const style = {
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: `${gap || 0}px`,
};
if (!isUndefined(rows)) {
style['gridTemplateRows'] = `repeat(${rows}, 1fr)`;
}
return style;
}, [columns, rows, gap]);
return (
<div className="brick-grid-system" style={containerStyle}>
{renderChildren()}
</div>
);
};
export default Container;
其实上面代码都很简单,现在唯一要做的就是 how to calc child's gridArea,这步搞定咱们的栅格系统就差不多竣工了。
实现renderChildren
说实话,这里走了不少弯路,最后的效果都不如人意,尝试多次之后,想到了用二维矩阵的方式去对应我们的布局。
- 根据grid的行列配置,生成对应的二维数组, 如24列 3行的grid就是[Array(24) * 3], 用0填充整个二维数组,初始效果如下
- 把我们创建好的二维数组想象成画布,被children占位之后,填充对应的index,就会生成一个和效果图相对应的二维数组,是不是有那味了
所以我们现在需要做的就是动态遍历children,根据每个child的 col、row、full值去动态的计算二维数组中能放下child的位置,并填充我们的二维矩阵,这样就能计算出每个child所需要的gridArea属性了
- renderChildren
export type Matrix = Array<Array<number>>;
function renderChildren() {
// 这里我们没有规定matrix的高度,动态更新,节省内存
const matrix: Matrix = [];
return React.Children.map(
children,
(child: React.ReactElement, index: number) => {
const { col, row, full } = child.props;
// 动态扩张高度 尽量大于需要的row
addMatrixData(matrix, row, columns);
// 计算出最第一个可填充的位置
let points = findAvailable(matrix, row, col, columns, full);
// 设置矩阵内容
setMatrixData(matrix, points, index + 1, full, columns);
// 转换成gridArea
const gridArea = getGridAreaFromPoints(points);
// 返回children
return React.cloneElement(child, { gridArea });
},
);
}
- addMatrixData 為矩陣添加數據
function addMatrixData(
arr: Matrix,
rows: number,
cols: number,
value = 0,
) {
for (let i = 0; i < rows; i++) {
arr.push(Array.from({ length: cols }).fill(value) as Array<number>);
}
}
- findAvailable 找到矩陣中最符合的位置(核心)
思路:现在我们有了一个二维矩阵,里面的值要么是没有被填充的部分(值为0),要么是被填充的部分(值为节点index,或者任何真值,用index只是为了更形象地展示被填充的情况),还有节点的rows(行高)和cols(列高),是否占用整行。查找开始前,我们声明两个变量 accRows和accCols分别表示遍历过程中符合条件的连续区间大小,如果 accRows === rows && accCols === cols 条件满足,则表示找到了我们想要的位置,如一格3行4列的元素,如果在二维数组中找到一块3 * 4 的连续未被填充的区间,就是我们想要的位置。
function findAvailable(
matrix: Matrix,
rows: number,
cols: number,
columns: number,
full: boolean = false
) {
// 两层遍历,外面代表二维数组的行遍历,里面则是列遍历
let accRows = 0,accCols = 0;
// rowStart 行开始的位置,外层循环且accRows为0的时候会被赋值, 每次内循环如果accCols不等于cols则清零,rowStart会从下一行开始
// colStart 列开始的位置 默认从0
// colEnd 列结束的位置
let rowStart,colStart = 0,colEnd;
for (let r = 0; r < matrix.length; r++) {
// accRows为0说明查找开始或上一轮查找失败 初始化rowStart为外循环的指针
if (accRows === 0) {
rowStart = r;
}
for (let c = colStart; c < columns; c++) {
// 判断可用的列数 如果小于元素的cols则就是装不下,重置colStart 结束本次内循环
const usableCols = columns - colStart;
if ((cols > usableCols) || full && (usableCols < columns)) {
colStart = 0;
break;
}
// 如果accCols、accRows都为0 开始或上一轮查找失败 初始化colStart为当前内循环的指针
if (accCols === 0 && accRows === 0) {
colStart = c;
}
// 当前元素的值
const val = matrix[r][c];
if (!val) {
// 未被占用的情况 accCols累加1
accCols += 1;
if (accCols === cols) {
// 如果 accCols === cols则说明找到了一行连续区间,记录下colEnd
colEnd = c;
break;
}
} else {
// 被占用的情况 重置accCols
accCols = 0;
}
}
// 内存循环结束,外层循环继续
if (accCols === cols) {
// 内循环满足条件后跳出来的情况
// accRows累加1
accRows += 1;
if (accRows === rows) {
// 满足条件的区间被找到,返回对应值,结束所有遍历
return { rowStart, colStart, rowEnd: r, colEnd };
}
// 重置accCols
accCols = 0;
} else {
// 未找到一行连续区间,重置rowStart和accRows
rowStart = r;
accRows = 0;
}
}
return null;
}
- setMatrixData 設置矩陣内容
export type Points = {
rowStart: number;
colStart: number;
rowEnd: number;
colEnd: number;
};
function setMatrixData(
matrix: Matrix,
points: Points,
value = 1,
full = false,
columns = DEFAULT_SYSTEM_COL,
) {
for (let r = points.rowStart; r <= points.rowEnd; r++) {
for (let c = points.colStart; c <= (full ? columns : points.colEnd); c++) {
matrix[r][c] = value;
}
}
}
- getGridAreaFromPoints 生成gridArea
function getGridAreaFromPoints(points: Points) {
return `${points.rowStart + 1} / ${points.colStart + 1} / ${points.rowEnd +
2} / ${points.colEnd + 2}`;
}
这样通过renderChildren渲染出来的children就有了我们需要的gridArea样式,完结撒花,心情舒畅~