使用視口分段 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);

另見