anjia / blog

博客,积累与沉淀
107 stars 4 forks source link

CSS Layout API #26

Open anjia opened 5 years ago

anjia commented 5 years ago

简介

CSS Layout API 能让网页开发人员写自己的布局算法。它是 CSS Houdini #23 的一部分

这样,我们在 CSS 里写display属性值的时候,就不局限于浏览器已经支持的那几种布局了,诸如display: blockdisplay: flex等,我们可以写display: layout(myLayout)

Layout API 里的所有内容都在 Logical Coordinate System(逻辑坐标系统)里计算。这样的话,网站的书写模式就可以自动影响到你写的布局了。

对于熟悉文本从左到右书写的开发人员来说,Logical Coordinate System 到我们日常用到的名词的对应关系如下:

Logical 在CSS里写的
inlineSize width
inlineStart left
inlineEnd right
blockSize height
blockStart top
blockEnd bottom

知道这个名词对应关系有什么意义呢?等会在编写 Layout Worklet 的时候,你会看到代码里大量用到了第一列的属性值。

参考 https://github.com/w3c/css-houdini-drafts/blob/master/css-layout-api/EXPLAINER.md

anjia commented 5 years ago

接下来,我们用一个例子来理解下 Layout API。

例子

它最终的运行效果是:
图1. 宽度自适应(若无图,请戳链接


图2.1 接受参数,eg.间距(若无图,请戳链接


图2.2 接受参数,eg.列数(若无图,请戳链接

完整代码见 src/css-layout-api/demo1.masonry

关键代码如下,建议配合着代码注释看,方便理解。

在 index.html 里

<style>
  ul {
    --padding: 5;
    --columns: 3;
    display: layout(masonry);
  }
</style>

<!-- 内含 10 个高度不等的 li -->
<ul>...</ul>

<script>
  if ('layoutWorklet' in CSS) {
    // 把 module script 添加到 Layout Worklet 里
    CSS.layoutWorklet.addModule('my_layout_masonry.js');
  }
</script>

在 my_layout_masonry.js 里

registerLayout('masonry', class {
  static get inputProperties() {
    return [ '--padding', '--columns' ]; 
  }

  *intrinsicSizes() { /* TODO */ }

  /**
   * 渲染引擎,在浏览器的layou 阶段时的回调
   * @param children 要执行layout元素的子元素列表
   * @param edges 在 logical coordinate system 里的 borders, scrollbar 和 padding 的大小
   * @param constraints 生成的片段应该满足的条件,该对象里提前计算了当前layout的一些属性。
   *                    eg. inline-size (width), block-size (height)
   * @param styleMap 当前layout的只读style
   */
  *layout(children, edges, constraints, styleMap) {
    // 1. 确定当前layout的内部大小, width
    const inlineSize = constraints.fixedInlineSize;

    const padding = parseInt(styleMap.get('--padding').toString());
    const columnValue = styleMap.get('--columns').toString();

    let columns = parseInt(columnValue);
    if (columnValue == 'auto' || !columns) {
      columns = Math.ceil(inlineSize / 350); // 默认每个宽350px
    }

    const childInlineSize = (inlineSize - ((columns + 1) * padding)) / columns;
    const childFragments = yield children.map((child) => {
      // 2. 对子节点进行布局,根据 columns
      return child.layoutNextFragment({fixedInlineSize: childInlineSize});
    });

    // 3. 算出'auto'块的大小。就能知道子元素的最大 height
    let autoBlockSize = 0;
    const columnOffsets = Array(columns).fill(0);
    for (let childFragment of childFragments) {
      const min = columnOffsets.reduce((acc, val, idx) => {
        if (!acc || val < acc.val) {
          return {idx, val};
        }

        return acc;
      }, {val: +Infinity, idx: -1});

      // 设置相对于父元素的 offset(除这两之外其它的属性都是只读的)
      childFragment.inlineOffset = padding + (childInlineSize + padding) * min.idx;
      childFragment.blockOffset = padding + min.val;

      columnOffsets[min.idx] = childFragment.blockOffset + childFragment.blockSize;
      autoBlockSize = Math.max(autoBlockSize, columnOffsets[min.idx] + padding);
    }

    // 4. 返回 fragment
    return {autoBlockSize, childFragments};
  }
});

参考 https://github.com/GoogleChromeLabs/houdini-samples