为双屏和可折叠设备构建网页布局


自从三星 Galaxy FoldSurface Duo 问世以来,已经过去了两年多。此后,Surface Duo 2、三星 Galaxy Z Fold 3Galaxy Z Flip 3 相继推出市场。可折叠设备现已上市销售,并且已经被消费者使用。对于开发者而言,这提供了一个机会,可以开始探索这一新型设备类别,以及响应式设计的下一步演进。

最初为双屏设备构建布局设计的 API 在开发者使用浏览器原始试验中的反馈后进行了多次修订。这些 API 的美妙之处在于,它们与现有概念相集成,因此设计师和开发者无需花费时间学习新概念,而是可以专注于如何为双屏设备构建更佳的用户体验。

使用新 CSS 媒体特性检测可折叠设备

双屏和可折叠设备只是响应式设计的下一步,因此它们被视为另一个响应式设计的目标,开发者可以使用媒体特性为其设计样式。我们已经使用媒体特性和查询来为桌面、平板电脑和手机进行样式设计,现在我们有了 CSS Viewport Segments(视口段)媒体特性来专门为可折叠和双屏设备设计样式。

horizontal-viewport-segments(水平视口段)
视口段媒体查询可以有两个值。第一个是 horizontal-viewport-segments,它代表设备的状态,此时设备铰链为垂直状态,并且视口被硬件铰链或折叠分割成列。
一个双屏设备的插图,显示屏并排显示

horizontal-viewport-segment 用于定位铰链处于垂直折叠状态的设备。

要为这种方向下的可折叠设备提供专门的样式,我们可以编写如下代码:

1
2
3
@media (horizontal-viewport-segments: 2) {
// 专门为该方向设备编写的样式
}

这个整数表示设备方向中存在的视口数量。当设备处于像书一样的垂直折叠状态时,水平方向上有两个独立的视口,而垂直方向上只有一个视口。

我们还可以结合媒体查询来定位双屏设备和特定的视口宽度,从而提供特定样式:

1
2
3
4
5
@media (horizontal-viewport-segments: 2) and (min-width: 540px) {
body {
background: yellow;
}
}

vertical-viewport-segments(垂直视口段)
视口段媒体特性的第二个值是 vertical-viewport-segments,它代表设备的状态,此时设备铰链为水平状态,并且硬件铰链将视口分成行。

一个可折叠设备的插图,显示屏上下叠放

vertical-viewport-segment 用于定位铰链处于水平折叠状态的设备。

要定位这种方向下旋转的设备,我们可以使用以下代码:

1
2
3
@media (vertical-viewport-segments: 2) {
// 专门为该方向设备编写的样式
}

使用 JavaScript 检测可折叠设备

有时你可能无法或不想使用 CSS 媒体查询来检测用户是否使用可折叠设备,此时 JavaScript API 就派上了用场。最初,提出了一个全新的 API,称为 Windows Segments Enumeration,但在开发者社区通过原始试验的反馈后,基于现有的 Visual Viewport API草案规范进行改进显得更为合理。

Viewport segments属性

Viewport segments表示位于相邻显示屏上的窗口区域。要检测双屏设备,可以使用以下代码查询 segments 属性:

1
const segments = window.visualViewport.segments;

此查询返回的值是一个 DOMRect 数组,指示有多少个视口段。如果只有一个视口段,查询将返回 null,这样设计是为了避免未来的兼容性问题,防止开发者使用 visualViewport.segments[0] 来定位单屏设备。

在双屏设备上,查询将返回 2 个 DOMRect,分别表示浏览器窗口跨越折叠部分时的 2 个视口。

存储在 segments 常量中的值是查询属性时设备状态的不可变快照,如果调整浏览器窗口大小或旋转设备,之前检索到的视口段将失效,必须通过 resizeorientation 事件(或两者)再次查询。

如果你将浏览器窗口调整为仅跨越一个显示区域,我们会触发 resize 事件。

如果旋转设备,这将同时触发 resizeorientation 事件,你可以使用这些事件再次查询属性,以获取当前的浏览器显示区域状态。

1
2
3
4
window.addEventListener("resize", function() {
const segments = window.visualViewport.segments;
console.log(segments.length); // 输出 1
});
何时使用 JavaScript API 与 CSS 媒体特性检测设备

CSS 媒体特性和 JavaScript segment 属性都可以检测双屏设备,但当你不使用 CSS 时,JavaScript 属性更为合适,比如在 Canvas2D 和 WebGL 中操作对象时。举个例子,你开发的游戏可以利用双屏。

使用 CSS env() 变量

除了 CSS 媒体特性,六个新的 CSS 环境变量已被引入,以帮助开发者计算显示区域的几何结构,计算硬件特性(如 Surface Duo 上的物理铰链)遮挡时的铰链区域的几何结构,并帮助将内容放置在每个显示区域的边界内。

这六个新环境变量如下:

1
2
3
4
5
6
env(viewport-segment-width <x> <y>);
env(viewport-segment-height <x> <y>);
env(viewport-segment-top <x> <y>);
env(viewport-segment-left <x> <y>);
env(viewport-segment-bottom <x> <y>);
env(viewport-segment-right <x> <y>);

xy 位置表示由硬件特性分隔的每个视口段所创建的二维网格,坐标 (0, 0) 从左上角段开始。

每个显示屏上的环境变量定义了视口逻辑上独立区域的位置和尺寸。

当设备处于垂直折叠状态,且视口并排时,左侧的视口段由 env(viewport-segment-width 0 0) 表示,右侧的视口段由 env(viewport-segment-width 1 0) 表示。如果将设备转为水平折叠状态,顶部的视口段由 env(viewport-segment-height 0 0) 表示,底部的视口段由 env(viewport-segment-height 0 1) 表示。

使用 env(viewport-segment-width)env(viewport-segment-height) 时,除了索引,我们还可以设置一个备用值,如下所示:

1
env(viewport-segment-width 0 0, 100%);

但这个备用值是可选的,是否包含由作者自行决定。

计算铰链宽度

当设备的铰链被硬件特性遮挡时,可以使用提供的环境变量来计算它。

在我们的例子中,设备处于垂直姿态,我们想找到铰链的宽度,以确保没有内容被遮挡。我们可以通过从右侧显示的左视口段减去左侧显示的右视口段来计算:

1
calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0));
使用 CSS env() 变量放置内容

我们可以使用 CSS 环境变量将内容放置在显示区域的边界内,特别是在你希望将内容直接放置在铰链或折叠处时非常有用。

在下面的示例中,我们将在左侧第一个显示区域的铰链处放置一张图片。这个区域是视口的右侧段,因此我们将使用 viewport-segment-right 来放置图片,代码如下:

1
2
3
4
5
6
7
8
9
10
img {
max-width: 400px;
}

@media (horizontal-viewport-segments: 2) {
img {
position: absolute;
left: env(viewport-segment-right 0 0);
}
}

如果在 Edge 开发者工具中以 Surface Duo 模式模拟屏幕,我们会得到如下布局:

一个在浏览器中模拟的 Surface Duo 布局,图片位于右侧显示屏,文字位于左侧 ,最初使用环境变量将图片放置在布局中时,它出现在了错误的显示区域。

这并不是我们想要的效果。图片应该位于左侧的显示区域。

由于图片的 position 是绝对定位,并且使用了 left 属性,图片的左边缘对齐到了 viewport-segment-right 显示区域。

我们需要从环境变量中减去图片的宽度,才能将图片对齐到正确的铰链边缘:

1
2
3
4
5
6
7
8
9
10
img {
max-width: 400px;
}

@media (horizontal-viewport-segments: 2) {
img {
position: absolute;
left: calc(env(viewport-segment-right 0 0) - 400px);
}
}

一个在浏览器中模拟的 Surface Duo 布局,文字和图片都位于左侧显示屏
从视口段中减去图片的宽度后,它就会正确地放置在左侧显示屏的铰链处。

现在我们已经将图片放置在我们想要的位置。关于如何将项目对齐到铰链的其他示例,你可以查看这个简单的盒子演示。打开 Edge Developer Tools > Device Emulation,然后选择 Surface Duo,并确保你的 Duo 模拟处于正确的方向姿态。

整合一切:让我们构建一个能够适应双屏设备的食谱页面

作为一个经常在做饭时使用手机的人,能够在双屏设备上自适应的食谱网站对我来说将非常有帮助。让我们一步步地探讨如何让一个单独的食谱页面适应双屏设备。

我首先要思考如何分块我的主要内容。通常,我至少会看到食谱标题、份量、烹饪时间、一张或多张图片、食材列表以及制作步骤。

当我绘制出我的线框图时,我得到了以下布局:

一张我网站在桌面上应该如何布局的草图

标准的桌面端食谱页面布局。

我希望在页面顶部显示标题和食谱详情,接着是占据整个内容宽度的图片,然后是食材列表和食谱步骤。我不想把后两部分内容堆叠在一起,因为如果堆叠,食材列表的右侧将会有大量空白,因此我希望步骤部分能位于食材旁边,形成图片下方的两列布局。

使用 CSS Grid 还是 Flexbox 来布局?

我已经确定了在普通桌面屏幕上的布局,并且有多种实现这种布局和内容分组的方法,但在编码之前,我需要考虑如何将其适配到双屏设备。根据桌面视图的草图,我可以使用 Flexbox 和 CSS Grid 的组合来实现想要的布局,将食材和步骤分组在一个 Flex 容器中。但让我再画一下如何在双屏设备上显示我的页面。

一张网站在双屏设备上的草图

在竖折位置的可折叠设备上的理想布局是将内容按显示屏分割,以避免被铰链遮挡。

如果我想要更灵活的布局,就不能将食材和步骤分组在 Flex 容器中,否则无论图片放在哪一列,另一列都会出现大量空白。

一张使用 Flexbox 在双屏设备上布局的草图,内容之间产生了较大的空隙
如果我只使用 Flexbox,这个布局会产生一些我想避免的空隙。

添加我们的内容

我将仅使用 CSS Grid 来为桌面和双屏布局构建页面。因此,让我们开始构建内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<main>
<section class="recipe">
<div class="recipe-meta">
… <!—Contains our recipe title, yield and servings -->
</div>
<img src="imgs/pasta.jpg" alt="Pasta carbonara photographed from above on a rustic plate" />
<div class="recipe-details__ingredients">
…<!— Contains our ingredients list -->
</div>
<div class="recipe-details__preparation">
… <!— Contains our list of steps to put the ingredients together -->
</div>
</section>
</main>

接下来,让我们构建页面的结构。我将定义我的网格:我只需要三列,并且希望这些列占据容器的相等份额。

1
2
3
4
.recipe {
display: grid;
grid-template-columns: repeat(3, 1fr);
}

然后,我将定义行,使用 grid-auto-rowsminmax,以便行的最小高度为 175px,最大高度为内容最大高度。

1
grid-auto-rows: minmax(175px, max-content);

接着,我会添加一些其他属性:网格间距、最大内容宽度,以及一个 margin 来让布局在页面上居中。

1
2
3
4
grid-gap: 1rem;
max-width: 64rem;
margin: 0 auto;
}

然后,我会将内容放入定义的网格中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.recipe-meta {
grid-column: 1 / 4;
}

.recipe-meta p {
margin: 0;
}

img {
width: 100%;
grid-column: 1 / 4;
}

.recipe-details__ingredients {
grid-row: 3;
}

.recipe-details__preparation {
grid-column: 2 / 4;
grid-row: 3;
}

这样就根据我的草图实现了布局:

带有 CSS Grid 引导线的桌面端网页布局截图
在桌面端渲染时布局如预期所示。

很好!但是我的双屏布局呢?让我们深入研究,从 horizontal-viewport 媒体特性和双屏布局的网格开始。

使用媒体查询并调整容器布局

首先,这是我当前在双屏设备上的布局:

一张网站在 Surface Duo 上模拟的截图
如果用户希望将浏览器跨越两个显示屏,而未实现任何双屏代码时,页面将显示的样子。

如果我们向下滚动:
一张网站在 Surface Duo 上模拟的截图,内容被设备铰链遮挡
如果用户选择跨越两个显示屏,内容会被铰链遮挡。

效果不佳。我们的内容被铰链遮挡了,所以我需要重新定义我的网格布局。

对于我的网格列,我仍将使用三列,但我希望第一列占据左边的整个显示区域,另外两列则分布在右边的显示区域。因此,我将使用 CSS 环境变量 env(viewport-segment-width 0 0),它告诉浏览器我的第一列应该占据左侧显示区域的整个视口。

1
2
3
4
5
6
7
8
9
10
11
12
13
@media (horizontal-viewport-segments: 2) { 

/* 针对小屏幕的主体样式 */
body {
font: 1.3em/1.8 base, 'Playfair Display', serif;
margin: 0;
}

.recipe {
grid-template-columns: env(viewport-segment-width 0 0) 1fr 1fr;
grid-template-rows: repeat(2, 175px) minmax(175px, max-content);
}
}

对于行,我希望在布局上有更多的灵活性,因此我将重复两行 175px 的高度,这大致是食谱标题、份量和时间信息容器的高度,接下来的行将匹配我在最初网格中定义的行。

当我在 DevTools 中检查我的设计时,我发现我最初为食谱容器设置的宽度和边距将我想对齐到视口段的网格线推到了右边的显示区域。

一张网站在 Surface Duo 上模拟的截图,布局分布在两个显示区域,内容没有被遮挡
在添加代码后,内容不再被遮挡,但仍需要进行间距调整。

为了解决这个问题,我将重置我的边距和最大宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@media (horizontal-viewport-segments: 2) { 

/* Body styles for smaller screens */
body {
font: 1.3em/1.8 base, 'Playfair Display', serif;
margin: 0;
}

.recipe {
grid-template-columns: env(viewport-segment-width 0 0 1fr 1fr;
grid-template-rows: repeat(2, 175px) minmax(175px, max-content);
}

}

一张网站在 Surface Duo 上的截图,边距和填充重置后
重置边距和填充后,右边的显示区域出现了内容遮挡。

接下来,我将内容放置到网格中并调整布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.recipe-meta {
grid-column: 1 / 2;
padding: 0 2rem;
}

img {
grid-column: 2 / 4;
grid-row: 1 / 3;
width: 100%;
height: 100%;
object-fit: cover;
/* 确保图片保持在网格线内 */
}

.recipe-details__ingredients {
grid-row: 2;
padding: 0 2rem;
}

.recipe-details__preparation {
grid-column: 2 / 4;
grid-row: 3;
padding: 0 2rem 0 3rem;
}

我已经为内容添加了填充,除了图片,我希望它跨越整个视口。由于网格线从物理铰链下方开始,对于图片下方的内容,我希望增加额外的填充,以确保左侧和其他内容的填充保持一致。如果我不增加额外的填充,它会离铰链太近。因为我已经设置了 1rem 的网格间距,而我希望将填充增加一倍,所以我会添加 3rem 而不是 4rem,这样可以给我们一个在双屏设备上的最终布局:

最终调整后的布局,添加了适当的填充和边距以适应双屏设备
我可以重新添加合适的填充,以便在有物理铰链的设备上内容不会被遮挡。

通过对 CSS 进行一些小的调整并使用新的媒体特性之一,我们已经实现了一个适应双屏设备的布局。想要体验这个效果,可以在 Edge 或基于 Chromium 的浏览器中查看这个演示网站,并打开浏览器的开发者工具以模拟 Surface Duo。如果你在 Chrome 中打开该网站,请确保在 chrome://flags 下启用了实验性 Web 平台功能标志,以便正确显示演示内容。

单屏响应式设计细节

为了确保我们考虑到小尺寸的单屏设备,我为手机布局选择的代码使用了 flexbox,并将所有内容放入一列中:

1
2
3
4
5
6
7
8
9
10
11
12
@media (max-width: 48rem) {

body {
font: 1.3em/1.8 base, 'Playfair Display', serif;
}

.recipe-details {
display: flex;
flex-direction: column;
}

}
没有设备的API浏览器可用性和测试

这些双屏 API 已经默认在 Microsoft Edge 和 Android 版 Edge 97 版本及更高版本中可用。这些 API 也计划在其他 Chromium 浏览器中推出,但具体日期尚未确定。要在 Chrome 中启用这些 API,请进入 chrome://flags 并启用实验性 Web 平台功能。

虽然这些设备相对较新,但许多已经进入了第二代和第三代,越来越多的公司正在投资。如果你无法获得物理设备,最好的测试方式是在浏览器开发者工具中。我已经在仿真工具和 Surface Duo 上测试过我的网站,Duo 的仿真工具几乎和真实设备一致。我的设计在设备和开发者工具中看起来相同。这使得为双屏设备构建和设计与为桌面和单屏移动设备开发同样简单。

如果你使用的桌面或设备不支持这些 API,可以为 Visual Viewport 段属性使用 polyfill。目前还没有适用于 CSS 媒体查询的 API。当前市面上的双屏设备基于 Android,这些 API 计划在 Android 上的 Chromium 浏览器中提供。

如果折叠设备的浏览器不支持这些功能,你可以使用 polyfill,或者确保你的网站在单屏设备上依然有良好的表现。用户可以自由选择如何在双屏设备上显示网站,他们可以跨越两个屏幕显示,也可以只跨一个屏幕。如果选择后者,它将像在单屏平板或手机上一样显示。即使你的网站没有双屏实现,用户仍然可以选择单屏显示。双屏 API 提供了一种渐进式增强的方式,为拥有这些设备的用户带来更好的体验。

结语

双屏设备是响应式设计的下一个发展方向。如果你有一个 PWA 或网站,现有的 API 让集成到代码库中变得无缝。此外,还有其他构建双屏设备应用程序的方法,你可以在 Surface Duo 文档中查看。这是网页布局的一个激动人心的时刻,双屏设备为创造性设计提供了更多机会。

更多资源