CSS Layout API:自定义布局算法实现瀑布流(Masonry)与约束布局

CSS Layout API:自定义布局算法实现瀑布流(Masonry)与约束布局

大家好,今天我们来深入探讨CSS Layout API,并利用它来实现两种常见的、传统上需要JavaScript辅助才能实现的布局:瀑布流(Masonry)和约束布局。CSS Layout API 赋予了我们直接在CSS中定义布局算法的能力,这不仅提高了性能,也使得代码更加简洁易维护。

1. CSS Layout API 简介

CSS Layout API,也称为 Houdini Layout API,允许开发者使用JavaScript编写自定义布局算法,并通过CSS来调用和控制这些算法。它主要包含以下几个核心概念:

  • CSS.layoutWorklet.addModule(): 用于注册布局工作模块,该模块包含自定义布局算法的JavaScript代码。
  • registerLayout(): 在布局工作模块中,使用该函数来注册一个布局类,该类定义了布局算法的具体实现。
  • layout(): 布局类中的核心方法,负责计算元素的位置和大小。浏览器会调用这个方法来进行布局计算。
  • intrinsicSizes(): (可选) 布局类中的方法,用于提供布局的固有大小,这对于某些布局(如自适应布局)非常重要。
  • CSS display: layout(my-layout): 在CSS中,使用display属性来指定元素使用自定义布局。my-layout是使用registerLayout()注册的布局名称。
  • CSS layout-children: ...: 该属性可以控制布局算法对子元素的布局方式,比如是顺序布局还是并行布局。
  • Input Properties: 通过 CSS custom properties (variables) 传递参数给自定义布局算法,允许我们通过 CSS 控制布局行为。

2. 环境配置与基础代码结构

在使用CSS Layout API之前,你需要确保你的浏览器支持它。目前,Chrome和Edge等浏览器已经原生支持。你可能需要在 chrome://flags 中启用 "Experimental Web Platform features" 标志。

一个典型的CSS Layout API项目包含以下几个文件:

  • index.html: 包含HTML结构和CSS样式,以及加载布局工作模块的<script>标签。
  • layout.js: 包含自定义布局算法的JavaScript代码。
  • style.css: 包含CSS样式,用于定义布局容器和子元素的样式,以及调用自定义布局。

index.html 示例:

<!DOCTYPE html>
<html>
<head>
  <title>CSS Layout API Demo</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <div>Item 1</div>
    <div>Item 2</div>
    <div>Item 3</div>
    <div>Item 4</div>
    <div>Item 5</div>
    <div>Item 6</div>
  </div>
  <script>
    if ('layoutWorklet' in CSS) {
      CSS.layoutWorklet.addModule('layout.js');
    } else {
      console.warn('CSS Layout API not supported in this browser.');
    }
  </script>
</body>
</html>

layout.js 示例:

registerLayout('my-layout', class {
  static get inputProperties() { return []; } // 接收的 CSS 属性列表

  async layout(children, edges, constraints, styleMap) {
    // 布局算法实现
    console.log("layout function called!");
    return { autoBlockSize: constraints.autoBlockSize, childInlineSizes: [] };
  }
});

style.css 示例:

.container {
  display: layout(my-layout);
}

.container > div {
  width: 100px;
  height: 100px;
  background-color: lightblue;
  border: 1px solid black;
}

这个基础代码结构只是一个起点。我们需要在layout.js中的layout()方法中编写实际的布局算法。

3. 实现瀑布流布局(Masonry)

现在,我们来实现一个简单的瀑布流布局。瀑布流布局将元素排列成多列,并尽可能地填充空白空间。

layout.js (瀑布流):

registerLayout('masonry-layout', class {
  static get inputProperties() {
    return ['--masonry-column-count', '--masonry-item-margin'];
  }

  async layout(children, edges, constraints, styleMap) {
    const columnCount = parseInt(styleMap.get('--masonry-column-count').toString()) || 3; // 默认3列
    const itemMargin = parseInt(styleMap.get('--masonry-item-margin').toString()) || 10; // 默认10px margin
    const columnWidth = (constraints.fixedInlineSize - (columnCount - 1) * itemMargin) / columnCount;
    const columnHeights = new Array(columnCount).fill(0);

    const childInlineSizes = [];
    const childBlockSizes = [];

    for (let child of children) {
      const availableColumn = columnHeights.indexOf(Math.min(...columnHeights)); // 找到最短的列
      const x = availableColumn * (columnWidth + itemMargin);
      const y = columnHeights[availableColumn];

      // 假设每个item的高度是固定的, 如果需要动态计算高度,需要使用child.intrinsicSizes()
      // 并根据内容来计算
      const blockSize = 150; // 假设item高度为150px

      childInlineSizes.push({inlineSize: columnWidth, blockSize: blockSize});

      columnHeights[availableColumn] += blockSize + itemMargin;

      child.styleMap.set('top', CSS.px(y));
      child.styleMap.set('left', CSS.px(x));
    }

    const autoBlockSize = Math.max(...columnHeights) ;

    return { autoBlockSize, childInlineSizes };
  }
});

style.css (瀑布流):

.container {
  display: layout(masonry-layout);
  position: relative; /*  必须设置,否则item的定位无效 */
  width: 800px;
  --masonry-column-count: 4;
  --masonry-item-margin: 15;
}

.container > div {
  position: absolute; /*  必须设置,让js控制位置 */
  width: 100px; /*  这个宽度会被layout.js中的columnWidth覆盖 */
  height: auto; /*  高度会被layout.js覆盖 */
  background-color: lightblue;
  border: 1px solid black;
  box-sizing: border-box;
  padding: 10px;
}

代码解释:

  1. inputProperties: 我们定义了两个CSS自定义属性:--masonry-column-count--masonry-item-margin,分别用于控制列数和项目之间的间距。
  2. layout():
    • 首先,从styleMap中获取自定义属性的值,并设置默认值。
    • 计算每列的宽度columnWidth,确保所有列的总宽度等于容器的固定宽度。
    • 创建一个数组columnHeights来跟踪每列的高度。
    • 遍历所有子元素,找到高度最小的列,并将元素放置在该列中。
    • 更新columnHeights数组,并将元素的位置信息添加到childInlineSizes中。
    • 使用child.styleMap.set()设置每个元素的位置,这需要子元素position设置为absolute。
    • 最后,计算容器的自动高度autoBlockSize,并返回结果。

关键点:

  • position: absolute: 必须设置子元素的positionabsolute,才能通过JavaScript来控制它们的位置。
  • child.styleMap.set(): 使用这个方法来设置子元素的样式,而不是直接修改DOM。
  • constraints.fixedInlineSize: 这是布局容器的宽度。
  • autoBlockSize: 这是布局容器的高度,layout函数需要返回这个值。
  • box-sizing: border-box: 建议使用,这样width和height包含padding和border。
  • intrinsicSizes(): 如果item的高度是动态的,需要使用child.intrinsicSizes()来获取每个item的固有高度,并根据内容来计算。

表格总结瀑布流实现的关键点:

步骤 说明 代码示例
定义CSS变量 定义CSS自定义属性,用于控制布局参数,例如列数和间距。 --masonry-column-count: 4;
--masonry-item-margin: 15;
获取CSS变量 layout()函数中,通过styleMap.get()获取CSS变量的值。 const columnCount = parseInt(styleMap.get('--masonry-column-count').toString()) || 3;
计算列宽 根据容器宽度和列数计算每列的宽度。 const columnWidth = (constraints.fixedInlineSize - (columnCount - 1) * itemMargin) / columnCount;
找到最短列 找到当前高度最小的列,并将元素放置在该列中。 const availableColumn = columnHeights.indexOf(Math.min(...columnHeights));
设置元素位置 使用child.styleMap.set()设置每个元素的位置。 child.styleMap.set('top', CSS.px(y));
child.styleMap.set('left', CSS.px(x));
计算容器高度 计算容器的自动高度,并返回。 const autoBlockSize = Math.max(...columnHeights) ;
return { autoBlockSize, childInlineSizes };
CSS样式设置 设置容器的display: layout(masonry-layout)position: relative,以及子元素的position: absolute .container {
display: layout(masonry-layout);
position: relative;
}
.container > div {
position: absolute;
}

4. 实现约束布局(Constraint Layout)

接下来,我们来实现一个简单的约束布局。约束布局允许我们使用约束条件来定义元素之间的关系,例如对齐、间距等。

layout.js (约束布局):

registerLayout('constraint-layout', class {
  static get inputProperties() {
    return ['--constraint-top', '--constraint-bottom', '--constraint-left', '--constraint-right', '--constraint-width', '--constraint-height'];
  }

  async layout(children, edges, constraints, styleMap) {
    const childInlineSizes = [];
    const autoBlockSize = constraints.autoBlockSize; // 默认为auto
    for (let child of children) {
      const top = parseInt(styleMap.get('--constraint-top')?.toString()) || null;
      const bottom = parseInt(styleMap.get('--constraint-bottom')?.toString()) || null;
      const left = parseInt(styleMap.get('--constraint-left')?.toString()) || null;
      const right = parseInt(styleMap.get('--constraint-right')?.toString()) || null;
      const width = parseInt(styleMap.get('--constraint-width')?.toString()) || null;
      const height = parseInt(styleMap.get('--constraint-height')?.toString()) || null;

      let x = 0;
      let y = 0;
      let inlineSize = constraints.fixedInlineSize;
      let blockSize = constraints.autoBlockSize;

      if (width !== null) {
        inlineSize = width;
      }

      if (height !== null) {
        blockSize = height;
      }

      if (left !== null) {
        x = left;
      } else if (right !== null) {
        x = constraints.fixedInlineSize - right - inlineSize;
      } else {
        // 居中
        x = (constraints.fixedInlineSize - inlineSize) / 2;
      }

      if (top !== null) {
        y = top;
      } else if (bottom !== null) {
        y = constraints.autoBlockSize - bottom - blockSize;
      } else {
        // 垂直居中
        y = (constraints.autoBlockSize - blockSize) / 2;
      }

      childInlineSizes.push({inlineSize: inlineSize, blockSize: blockSize});
      child.styleMap.set('top', CSS.px(y));
      child.styleMap.set('left', CSS.px(x));
      child.styleMap.set('width', CSS.px(inlineSize));
      child.styleMap.set('height', CSS.px(blockSize));

    }
    return { autoBlockSize, childInlineSizes };
  }
});

style.css (约束布局):

.container {
  display: layout(constraint-layout);
  position: relative;
  width: 500px;
  height: 300px;
  border: 1px solid red;
}

.container > div {
  position: absolute;
  background-color: lightgreen;
  border: 1px solid black;
  box-sizing: border-box;
}

.item1 {
  --constraint-top: 20;
  --constraint-left: 20;
  --constraint-width: 100;
  --constraint-height: 50;
}

.item2 {
  --constraint-bottom: 20;
  --constraint-right: 20;
  --constraint-width: 80;
  --constraint-height: 40;
}

.item3 {
  --constraint-width: 60;
  --constraint-height: 30;
}

代码解释:

  1. inputProperties: 我们定义了几个CSS自定义属性:--constraint-top--constraint-bottom--constraint-left--constraint-right--constraint-width--constraint-height,分别用于控制元素的位置和大小。
  2. layout():
    • styleMap中获取自定义属性的值。
    • 根据约束条件计算元素的位置和大小。如果同时指定了leftright,则优先使用left。如果同时指定了topbottom,则优先使用top。如果没有指定任何约束条件,则将元素居中显示。
    • 使用child.styleMap.set()设置每个元素的位置和大小。

关键点:

  • 约束布局的核心思想是使用约束条件来定义元素之间的关系。
  • 通过CSS自定义属性来传递约束条件。
  • layout()函数中,根据约束条件计算元素的位置和大小。

表格总结约束布局实现的关键点:

步骤 说明 代码示例
定义CSS变量 定义CSS自定义属性,用于控制元素的位置和大小,例如--constraint-top--constraint-bottom--constraint-left--constraint-right--constraint-width--constraint-height --constraint-top: 20;
--constraint-left: 20;
--constraint-width: 100;
--constraint-height: 50;
获取CSS变量 layout()函数中,通过styleMap.get()获取CSS变量的值。 const top = parseInt(styleMap.get('--constraint-top')?.toString()) || null;
计算位置大小 根据约束条件计算元素的位置和大小。如果同时指定了leftright,则优先使用left。如果同时指定了topbottom,则优先使用top。如果没有指定任何约束条件,则将元素居中显示。 javascript<br>if (left !== null) {<br> x = left;<br>} else if (right !== null) {<br> x = constraints.fixedInlineSize - right - inlineSize;<br>} else {<br> // 居中<br> x = (constraints.fixedInlineSize - inlineSize) / 2;<br>}<br><br>if (top !== null) {<br> y = top;<br>} else if (bottom !== null) {<br> y = constraints.autoBlockSize - bottom - blockSize;<br>} else {<br> // 垂直居中<br> y = (constraints.autoBlockSize - blockSize) / 2;<br>}
设置元素样式 使用child.styleMap.set()设置每个元素的位置和大小。 child.styleMap.set('top', CSS.px(y));
child.styleMap.set('left', CSS.px(x));
child.styleMap.set('width', CSS.px(inlineSize));
child.styleMap.set('height', CSS.px(blockSize));

5. 总结与展望

今天我们学习了如何使用CSS Layout API来实现瀑布流布局和约束布局。这两种布局方式在传统上都需要JavaScript来辅助实现,但是使用CSS Layout API,我们可以直接在CSS中定义布局算法,这不仅提高了性能,也使得代码更加简洁易维护。

尽管CSS Layout API 提供了强大的自定义布局能力,但它仍然处于发展阶段。在实际应用中,可能需要考虑以下因素:

  • 浏览器兼容性: 目前,CSS Layout API主要在Chrome和Edge等浏览器中得到支持。在使用时,需要考虑浏览器的兼容性问题,并提供备选方案。
  • 性能优化: 自定义布局算法的性能至关重要。在编写布局算法时,需要注意避免不必要的计算和重绘,以提高页面性能。
  • 调试: 调试自定义布局算法可能会比较困难。可以使用浏览器的开发者工具来调试JavaScript代码,并查看布局计算的结果。

未来,随着CSS Layout API的不断完善和普及,我们可以期待更多的自定义布局算法出现,从而为Web开发带来更多的可能性。
CSS Layout API 是一项强大的技术,它让我们能够创造出更加灵活和高效的Web布局。掌握它,将会大大提升你的前端开发能力。

6. 布局的未来,探索更多可能性

CSS Layout API 带来的不仅仅是瀑布流和约束布局,它打开了自定义布局的大门。 我们可以尝试更多复杂的布局,例如:

  • 圆形布局
  • 网格布局的变体
  • 基于内容的自适应布局

这些都将是未来探索的方向。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注