使用视口分段 API

本文介绍如何使用视口分段 API来创建针对不同视口分段大小和排列进行优化的响应式设计。

折叠设备的难题

折叠设备包括智能手机、平板电脑和笔记本电脑。有些向内折叠,显示屏折叠到设备内部;有些向外折叠,显示屏环绕设备。折叠设备有多种形式:有些有实际的折叠屏,而有些则有独立的屏幕,中间有物理铰链。它们可以横向使用,两个屏幕并排;也可以纵向使用,一个屏幕在上,一个屏幕在下。

无论哪种情况,折叠设备的显示屏都旨在作为同一显示表面的不同分段。虽然一个人使用的折叠设备可能看起来无缝且完全平放,类似于单分段视口,但另一个人可能会看到明显的接缝,以小于完全展开的平坦屏幕的角度使用。这带来了一些独特的挑战。您可以优化您的布局以适应整个显示器作为一个整体,但如何确保设计元素能够贴合不同的分段,而不是被分成两部分?如何防止内容被物理折叠/连接隐藏?

视口分段 API 提供了功能,允许您(在 CSS 和 JavaScript 中)检测用户的设备屏幕是否具有折叠或连接,不同分段的大小,它们是否相同大小,以及它们的朝向(并排或上下)。我们将在接下来的几节中介绍这些功能,然后通过一个完整的示例来展示它们的作用。

视口分段媒体功能

有两种媒体查询功能可用于测试设备是否具有特定数量水平或垂直排列的视口分段。它们看起来像这样:

css
/* Segments are laid out horizontally. */
@media (horizontal-viewport-segments: 2) {
  .wrapper {
    flex-direction: row;
  }

  /* ... */
}

/* Segments are laid out vertically. */
@media (vertical-viewport-segments: 2) {
  .wrapper {
    flex-direction: column;
  }

  /* ... */
}

horizontal-viewport-segments 媒体功能用于检测设备是否具有指定数量的水平排列的视口分段,而vertical-viewport-segments 媒体功能用于检测设备是否具有指定数量的垂直排列的视口分段。

视口分段环境变量

为了使布局精确地适应可用的视口分段,视口分段环境变量提供了对每个分段尺寸及其在整个视口内位置的访问。浏览器提供了[环境变量],允许访问每个分段的宽度和高度,以及它们顶部、右侧、底部和左侧边缘的偏移位置。

  • viewport-segment-width
  • viewport-segment-height
  • viewport-segment-top
  • viewport-segment-right
  • viewport-segment-bottom
  • viewport-segment-left

env()函数用于访问这些变量,其中变量名称和两个整数代表要返回值的分段索引。例如:

css
/* Return the width of the top/left segment */
env(viewport-segment-width 0 0)

/* Return the width of the right segment */
env(viewport-segment-width 1 0)

/* Return the width of the bottom segment */
env(viewport-segment-width 0 1)

这些索引都是 0 或更大的整数。第一个值表示分段的水平索引值,第二个值表示分段的垂直索引值。

Two device segment layouts; in a horizontal layout, 0 0 is the first segment and 1 0 is the second segment. In a vertical layout, the indices are 0 0 and 0 1

  • 在水平并排布局中,左侧分段由 0 0 表示,右侧分段由 1 0 表示。
  • 在垂直上下布局中,顶部分段由 0 0 表示,底部分段由 0 1 表示。

在布局中,您可以使用这些变量来设置您的容器,使其能够整齐地适应可用的分段。例如:

css
@media (horizontal-viewport-segments: 2) {
  .wrapper {
    display: grid;
    grid-template: "left fold right";
    grid-column: env(viewport-segment-width 0 0) env(viewport-segment-width 1 0);
  }
  .firstSection {
    grid-area: left;
  }
  .secondSection {
    grid-area: right;
  }
}

@media (vertical-viewport-segments: 2) {
  .wrapper {
    display: grid;
    grid-template:
      "top"
      "bottom";
    grid-row: env(viewport-segment-height 0 1) env(viewport-segment-width 0 0);
  }
  .firstSection {
    grid-area: top;
  }
  .secondSection {
    grid-area: bottom;
  }
}

在这里,我们根据视口分段是水平还是垂直排列,将外部包装器设置为水平或垂直网格布局。然后,我们将左侧和顶部单元格设置为第一个分段,并将第二个部分放置在右侧或底部网格单元格中。

我们可以添加一个空的中间“折叠”单元格,以防止内容被折叠遮挡。我们可以通过从整个视口大小中减去两个侧面的组合宽度或高度来计算其厚度,或者将中间单元格设置为 1fr

css
@media (horizontal-viewport-segments: 2) {
  .wrapper {
    grid-template: "left fold right";
    grid-column: env(viewport-segment-width 0 0)
      calc(
        100vw -
          (env(viewport-segment-width 0 0) + env(viewport-segment-width 1 0))
      )
      env(viewport-segment-width 1 0);
  }
}

@media (vertical-viewport-segments: 2) {
  .wrapper {
    grid-template:
      "top"
      "fold"
      "bottom";
    grid-row: env(viewport-segment-height 0 1) 1fr
      env(viewport-segment-width 0 0);
  }
}

在 JavaScript 中访问分段信息

您可以使用 window.viewport.segments 属性在 JavaScript 中访问分段信息,该属性返回一个 DOMRect 对象数组,提供对每个分段在整个视口内的 xy 坐标以及它们的 widthheight 的访问。

例如,此代码片段将遍历视口中的每个分段,并将一个字符串记录到控制台,详细说明索引号、宽度和高度。

js
const segments = window.viewport.segments;

segments.forEach((segment) =>
  console.log(
    `Segment ${segments.indexOf(segment)} is ${segment.width}px x ${segment.height}px`,
  ),
);

完整示例

让我们在实际示例中看看视口分段 API 的功能。您可以在视口分段 API 演示中看到我们的示例实时运行(也可以查看完整的源代码)。如果可能,请在真正的折叠设备上查看演示。能够直观地模拟折叠设备的多个分段的浏览器开发工具通常不包括物理分段的模拟。

注意:此示例改编自 Alexis Menard 和 Thomas Steiner 在 developer.chrome.com 上于 2024 年发布的折叠 API 的原始试验,采用 知识共享署名 4.0 许可证

我们将在接下来的几节中逐步介绍源代码。

HTML 结构

标记包含一个包装器 <div>,其中包含两个 <section> 元素,分别代表基本的列表视图和详细视图,以及一个代表折叠设备上两个分段之间折叠的 <div>

html
<div class="wrapper">
  <section class="list-view">
    <!-- ... -->
  </section>
  <div class="fold"></div>
  <section class="detail-view">
    <!-- ... -->
  </section>
</div>

选择性地为不同的分段方向应用布局

在我们的 CSS 中,我们结合使用了媒体查询和环境变量,以创建能够舒适地适应可用分段的响应式布局。

首先,我们使用 orientation 媒体查询测试来为包装器 <div> 的子项设置适当的 flexbox 布局,以应对各种情况——landscape 视口为 rowportrait 视口为 column。请注意,在这种情况下,我们也已将折叠 <div> 设置为一条细带,以充当两个内容容器之间的分隔符——在 landscape 布局中宽度为 20px,在 portrait 布局中高度为 20px

css
.wrapper {
  height: 100%;
  display: flex;
}

@media (orientation: landscape) {
  .wrapper {
    flex-direction: row;
  }

  .list-view,
  .detail-view {
    flex: 1;
  }

  .fold {
    height: 100%;
    width: 20px;
  }
}

@media (orientation: portrait) {
  .wrapper {
    flex-direction: column;
  }

  .list-view,
  .detail-view {
    flex: 1;
  }

  .fold {
    height: 20px;
  }
}

接下来,我们使用 horizontal-viewport-segments 媒体查询来处理分段并排的折叠设备的情况。

我们将外部包装器设置为水平 flexbox 布局,当视口分段水平排列时。我们将左侧容器的宽度设置为等于左侧分段的宽度(env(viewport-segment-width 0 0)),将右侧容器的宽度设置为等于右侧分段的宽度(env(viewport-segment-width 1 0))。为了计算折叠在两者之间占用的宽度,我们从右侧容器的左边缘偏移量中减去左侧容器的右边缘偏移量(calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0));)。

css
@media (horizontal-viewport-segments: 2) {
  .wrapper {
    flex-direction: row;
  }

  .list-view {
    width: env(viewport-segment-width 0 0);
  }

  .fold {
    width: calc(
      env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0)
    );
    background-color: black;
    height: 100%;
  }

  .detail-view {
    width: env(viewport-segment-width 1 0);
  }
}

最后,我们使用 vertical-viewport-segments 媒体查询来处理分段上下排列的折叠设备的情况。这使用了与上一个代码片段相同的方法,除了我们设置的是高度而不是宽度,并且使用高度/顶部/底部环境变量来返回所需的值。

css
@media (vertical-viewport-segments: 2) {
  .wrapper {
    flex-direction: column;
  }

  .list-view {
    height: env(viewport-segment-height 0 0);
  }

  .fold {
    width: 100%;
    height: calc(
      env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0)
    );
    background-color: black;
  }

  .detail-view {
    height: env(viewport-segment-height 0 1);
  }
}

使用 JavaScript 报告分段大小

我们还报告每个分段的尺寸,并在屏幕调整大小时,或在设备姿势或方向更改时更改这些值。

首先,我们获取对包装器 <div> 及其两个 <section> 元素子项的引用(这些是我们在 CSS 中放置在两个分段内的两个容器)。

js
const wrapperElem = document.querySelector(".wrapper");
const listViewElem = document.querySelector(".list-view");
const detailViewElem = document.querySelector(".detail-view");

接下来,我们定义一个 addSegmentOutput() 函数,该函数接受 segments 数组、索引号和元素引用作为参数。此函数将一个分段输出 <div> 追加到引用的元素。输出包括一个标题,其中包含视口分段的索引号及其尺寸。

js
function addSegmentOutput(segments, i, elem) {
  const segment = segments[i];

  const divElem = document.createElement("div");
  divElem.className = "segment-output";

  elem.appendChild(divElem);

  divElem.innerHTML = `<h2>Viewport segment ${i}</h2>
  <p>${segment.width}px x ${segment.height}px</p>`;
}

接下来,我们定义一个 reportSegments() 函数,该函数会删除任何先前追加的分段输出 <div> 元素,清空 <div>,然后调用先前定义的 addSegmentOutput() 函数,该函数基于使用window.viewport.segments 检索的设备分段数组。我们检查存在的段数。

  • 如果只存在一个分段,我们会运行一次 addSegmentOutput(),将一个分段输出 <div> 添加到包装器 <div>。这将报告整个视口的尺寸。
  • 如果存在两个分段,我们会运行两次 addSegmentOutput(),将一个分段输出 <div> 添加到每个 <section> 元素。这将报告每个分段输出 <div> 的父分段的尺寸。
js
function reportSegments() {
  // Remove all previous segment output elements before adding more
  document.querySelectorAll(".segment-output").forEach((elem) => elem.remove());

  const segments = window.viewport.segments;

  if (segments.length === 1) {
    addSegmentOutput(segments, 0, wrapperElem);
  } else {
    addSegmentOutput(segments, 0, listViewElem);
    addSegmentOutput(segments, 1, detailViewElem);
  }
}

最后,我们调用 reportSegments() 函数,并添加事件监听器以在几种不同的上下文中运行它:

  • 我们在全局作用域中运行一次,以便在页面加载时立即将分段报告添加到页面。
  • 我们在 resize 事件的基础上运行它,以便在窗口调整大小时(包括方向更改)更新分段报告。
    • 我们基于 DevicePosturechange 事件运行它,以便在设备姿势更改时更新分段报告。
js
reportSegments();
window.addEventListener("resize", reportSegments);
navigator.devicePosture.addEventListener("change", reportSegments);

另见