可用于双屏幕和折叠屏的Web API

发布于 大漠

早在去年五月份就在《聊聊安卓折叠屏给交互设计和开发带来的变化》一文中和大家初次聊了安卓折叠屏给交互设计和开发将会带来的变化。随着一年多来的变化,在Web开发中也有相应的API专门服务于双屏幕和折叠屏幕,有关于这方面的介绍,前段时间在《可折叠Web可能会给我们带来的变化》一文中和大家有过初步的介绍。虽然这两篇文章从不同的时间段,不同的角度阐述了折叠屏中的开发模式,但我想很多同学还是会因此感到困惑,甚至不知道如何为多屏幕或折叠屏幕这样的设备做相应的Web开发。那么今天,在这篇文章中抛开概念,只聊一些Web API,即 可用于双屏幕和折叠屏的Web API。如果你感兴趣的话,请继续往下阅读。

可用的Web API

随着各种新型的双屏幕设备(比如微软的Surface Duo)和可折叠屏幕(比如华为的Mate X,三星的Galaxy Fold)设备上市,做为Web开发人员是应该开始考虑让你的Web页面或Web应用能适配这些设备。

值得庆幸的是,到目前为止,有两个实验性的功能可以为我们所用,这将帮助Web开发人员有效地为自己的Web页面适配于多屏幕或折叠屏幕。

可折叠设备类型

目前在市场上有的或将来会有的各大终端厂商提供的多屏幕或可折叠设备如下图所示:

现在可以看到(或者说偶已看到的真机)的设备主要有:三星的 Galaxy Z Fold 2Galaxy Z Flip,华为的 Mate XMate Xs,微软的 Surface Duo等。我搜集了一些相关设备的参数,如下表所示:

设备名称 尺寸(inch 分辨率(px PPI 宽高比 备注
三星Galaxy Z Fold 2 6.23 2260 x 816 373 25:9 折叠状态(外部)
三星Galaxy Z Fold 2 7.6 2208 x 1768 373 22.5:18 展开状态 (内部)
三星Galaxy Z Flip 1.1 300 × 112 303 折叠状态(第二屏)
三星Galaxy Z Flip 6.7 2636 × 1080 425 21.9:9 展开状态(主屏)
Motorola Razr 2019 6.2 2142 x 876 373 21:9 展开状态(主屏)
Motorola Razr 2019 2.7 800 x 600 373 4:3 折叠状态(第二屏)
华为 Mate X(s) 8 2480 x 2200 414 8:7.1 展开状态
华为 Mate X(s) 6.6 2480 x 1148 414 19.5:9 折叠状态(前屏)
华为 Mate X(s) 6.38 2480 x 892 414 25:9 折叠状态(后屏)
微软 Surface Duo 8.1 2700 x 1800 401 3:2 展开状态
微软 Surface Duo 5.6 1800 x 1350 401 4:3 折叠状态
Royole Flexpai 7.8 1920 x 1440 308 4:3  
LG G8X ThinQ 6.4 2340 x 1080 403 19.5:9  

一般来说,可折叠设备有两种变体:双屏幕设备 和利用柔性显示技术的单屏幕设备。两者有很多共同点:它们都是便携的、多姿势的设备,允许用户旋转、翻转和折叠

在多屏幕或可折叠设备上,Web应用或Web页面在这些设备上的打开姿势也将会有所不同,应用可以单屏显示,也可以跨屏显示:

换句话说,我们的应用或页面要具备这种跨越屏幕的能力,也要具备响应这种跨越的能力,以及还可能需要具备逻辑分隔内容的能力等。

可以说,多屏幕或折叠屏设备开启了更广阔的屏幕空间以及用独特的姿势将用户带入到另一个世界。针对于这种设备,除了用户之外,对于UI设计师,用户体验师和Web开发人员都需要重新面临解锁前所未有的Web体验。这也将是近十年来,Web开发带来最大的变化之一,以及开发人员所要面临的最大挑战之一。

如果你也是一名Web开发人员的话,那就随我一起开启这段新的挑战之旅吧!

配置可折叠设备模拟器

对于Web开发者来说,首先需要的是一台真机用于测试,只有这样才能随时掌握自己开发的应用是否能适配于这样的设备。但现实是非常残酷的,目前为止,这样的终端设备价格都不便宜。

无设备,无真相,一切都是浮云~~

不过庆幸的是,我们可以有其他的方式来帮助我们构建折叠设备模拟器,让你开启新的生活。

先来看最简单的一种方式,就是使用在线Web版本的“Surface duo”模拟器:

类似于在浏览器的地址栏一样,输入你需要测试的地址,看到的效果如下:

因为小站并没有对多屏幕和折叠屏幕做适配处理,所以在模拟器的地址栏中输入小站网址,看到的效果就像上图那样。

由于没有真机,也无法确认Web版本在线模拟器与真机效果有多远差距。

除了在线Web版本模拟器之外,我们还可以在浏览器的开发者工具中使用双屏幕和可折叠设备的模拟器。到目前支持的浏览器主要有微软的Edge 86+(Microsoft Edge 86+)版本Chrome的Canary 87+版本

先来看Edge浏览器上如何开启双屏幕模拟器的设置。选择打开Edge浏览器,然后在地址栏输入edge://flags,并且搜索“Experimental Web Platform features”,将其设置为“已启用”状态:

重启Edge浏览器,并且开启开发者检测工具,在配置中开启双屏幕模拟器相关的设置:

这样就可以在Edge浏览器中模拟出双屏和折叠屏的效果:

我们来看一个支持双屏或折叠屏幕的Demo。很简单,在Edge浏览器的地址栏输入“https://conceptdev.github.io/web-samples/dual-screen-css/boxes.html”网址:

Chrome Canary的开启多屏和折叠屏幕的模拟器设置和Edge的差不多。首先你要确认你的Canary版本是87+版本,然后打开该浏览器,并在地址栏中输入:chrome://flags/。搜索“Experimental Web Platform features”,并且将其设置为“Enabled”:

并且开启开发者检测工具,在配置中开启双屏幕模拟器相关的设置:

这样就可以对多屏幕或折叠屏幕进行模拟测试:

如果你担心在浏览器的模拟器上看到的效果和真机有较在差异,那么还可以在本地电脑上安装安卓虚拟机。

有关于怎么安装,这里就不做详细阐述,感兴趣的话可以参阅下面相关教程:

从传统屏幕向双屏幕和可折叠屏幕过渡

大多数用户和开发者习惯的是传统屏幕的体验和开发,但作为一名Web开发者,我们有必要有这方面的意识:

随着双屏幕和可折叠屏幕的出现,这些设备的特性可以极大地增强用户体验

为了更好的阐述和演示多屏幕和可折叠屏幕是如何工作的,我将通过简单的Web应用带领大家了解这方面变化。

拿一个邮件查阅的应用来举例,比如说Gmail。我们习惯于“收件箱”、“邮件列表”和“邮件详情”几列并列显示,这也是常见的一种模式。这种模式对于大屏幕(比如PC端)而言,它的显示很自然,也很好,给用户的体验也很不错。

如果我们在双屏幕设备上打开该应用时,浏览器窗口会横跨两个显示区域(就是多屏幕的两个逻辑显示区域),整个视窗宽度可能与传统的平板设备相似。

要是Web应用未针对多屏幕和可折叠屏幕做修改的话,它也能在这些设备上显示,但是不是最佳。然而,我们可以针对性的做一些调整。比如将“收件箱”和“邮件列表”在多屏幕的第一个逻辑区域显示,“邮件详情”在多屏幕的另一个逻辑区域显示。这样一来,体验将会得到极大的改善,即 内容区既不会被设备铰链切断或遮蔽,也不会在柔性显示屏的折页上显示

用一张图来描述从传统的屏到多屏幕的过渡,会让大家更好的理解其中的差异:

为了实现我们最终想要的效果(上图最右侧的效果),将会为多屏幕和可折叠屏幕引入一个新的媒体查询特性和一组有关于折叠屏的预定义环境变量,这些变量允许Web开发人员将可折叠设备视为另一个响应式Web设计。基于这些特性之上,Web开发人员可以构建适合每一种设备类型的布局,而且不必严格依赖于特定的硬件参数(在《聊聊安卓折叠屏给交互设计和开发带来的变化》一文中提供的适配方案和案例,虽然可以在三星和华为的可折叠设备上实现适配布局,但依赖了特定的硬件参数)。也就是说,这些新特性提高了一定的灵活性,而且相应的提高了可伸缩性,因为它不再需要为每种新型设备类型做额外的、重复的工作。

CSS如何检测显示区域

前面我们知道,不管是双屏幕还是可折叠屏幕设备上,都有两个共性,一个“折叠区”,两个“逻辑显示区域”。对于开发人员来说,最重要的是要知道内容应该(或将)在哪个区域(逻辑显示区域)显示。

CSS新增了一个媒体查询特性screen-spanning,可以用来帮助Web开发人员检测“根视图”是否跨越多个相邻显示区域,并提供有关这些相邻显示区域配置的详细信息。

CSS的screen-spanning可以被指定为一个值,该值可以描述设备具有的折叠(或铰链)数量及其姿态。如果该设备不是可折叠设备,则值为none。如果它是可折叠的,它可以接受以下两个值中的一个:

  • single-fold-vertical:屏幕是水平的,布局视图跨越单个折叠(两个屏幕)并且折叠姿势是垂直时(分左右两边),这个值是匹配的
  • single-fold-horizontal:屏幕是垂直的,布局视图跨越单个折叠(两个屏幕)并且折叠姿势是水平时(分上下),这个值是匹配的

计算显示区域几何形状

在传统屏幕下,我们关注的只是Viewport的大小,一般情况之下,使用媒体查询就可以处理好。但是在双屏幕和可折叠屏幕下,开发者要注意的东西就多了,比如说Web内容是在哪个逻辑显示区域显示,比如说在折叠后隐藏部分Web内容等。为了帮助Web开发人员能很好的计算出每个逻辑显示区域的大小,并确保他们知道有多少内容(如果有的话)在正确的区域显示需要所展示的内容,CSS新增加了几个环境变量,而且这几个环境变量都和双屏幕或可折叠屏幕有关。

  • env(fold-top)
  • env(fold-left)
  • env(fold-width)
  • env(fold-height)

这些CSS环境变量的值是CSS像素,并且是相对于布局视图的(即在客户端坐标中,由CSSOM视图定义)。当不处于跨越状态时,这些值将被视为不存在,则会取env()函数的回退值。

在多屏幕和可折叠屏幕上增加电子邮件应用的用户体验

先来看优化前和优化后的效果对比图:

如果我们用CSS来写的话,大致会像下面这样:

@media screen and (min-width: 799px) {
    /* 
    * 1. PC端样式规则
    * 2. 平板电脑(比如iPad样式规则)
    */
}

@media screen and (min-width: 799px) and (screen-spanning: single-fold-vertical) {
    /* 
    * 1. main元素包含了收件箱(.navigation)、邮件列表(.inbox)和邮件详情(.detail)
    * 2. Flexbox布局
    */
    main {
        display: flex;
        flex-direction: row;
    }

    .navigation {
        /* 
         * 1. “收件箱”在双屏幕和可折叠屏幕的宽度 60px
         * 2. flex-basis设置为60px
        */
        flex-basis: 60px;
        flex-grow: 0;
        flex-shrink: 0;
    }

    .inbox {
        /* 
         * 1. “收件列表”的宽度,使用CSS的calc计算
         * 2. 收件列表(.inbox)的宽度= 逻辑显示区域1的宽度 - 60px (“收件箱(.navigation)”宽度)
         * 3. width = env(fold-left) - 60px
        */
        flex-basis: calc( env(fold-left) - 60px );
        
        /* 
         * 1. 有些硬件设备中间有一定的物理分隔间距 env(fold-width)
         * 2. 比如微软Surface Duo中间分隔间距: env(fold-width) = 28px
         * 3. 硬件设备没有物理分隔间距: env(fold-width) = 0px
        */
        margin-inline-end: env(fold-width);
        flex-grow: 0;
        flex-shrink: 0;
    }

    .detail {
        /* 
         * 1. 邮件详细(.detail)在第二逻辑显示区域显示
         * 2. 第二逻辑显示区域的宽度 100vw - 第一逻辑显区域宽度 - 物理间隔(第一和第二逻辑显示区域之间的间距)
        */
        flex-basis: calc( 100vw - (env(fold-left) + env(fold-width)) );
        flex-grow: 0;
        flex-shrink: 0;
    }
}

screen-spanning的Polyfill

到目前为止,CSS新增的媒体查询特性screen-spanning和有关于折叠设备预定的环境变量都处于草案的讨论阶段,还没有得到主流设备(比如我们前面所说的双屏幕设备和可折叠屏幕设备)的支持。也就是说,如果现在就希望在双屏幕和可折叠屏幕上使用这些特性,那么我们需要一个Polyfill。

screen-spanning的Polyfill是由@darktears提供的

这个Polyfill的使用很简单,可以在相应的项目中使用NPM来安装该Polyfill

» npm install --save spanning-css-polyfill

安装完之后,通过<script>将对应的spanning-css-polyfill.js引入到项目中:

<script type="module" src="/path/to/modules/spanning-css-polyfill.js"></script>

也可以使用import的方式引入:

import "/path/to/modules/spanning-css-polyfill/spanning-css-polyfill.js";

这样你就可以在CSS中使用screen-spanning这个新媒体查询特性和CSS环境变量fold-topfold-leftfold-widthfold-height

当然,你还可以手动改变显示(display)相关的配置。比如,通过导入FoldablesFeature对象:

import { FoldablesFeature } from '/path/to/modules/spanning-css-polyfill/spanning-css-polyfill.js';

可以通过FoldablesFeature对象来更新像spanningfoldSizebrowserShellSize等值。你也可以订阅change事件,以便spanning媒体查询特性或环境变量发生变更时得到相应的通知。

import { FoldablesFeature } from '/path/to/modules/spanning-css-polyfill/spanning-css-polyfill.js';

const foldablesFeat = new FoldablesFeature;

// 添加事件监听
foldablesFeat.onchange = () => console.log("change");

// 添加任意数量的事件监听器
foldablesFeat.addEventListener('change', () => console.log("change"));

// 更改单个值,导到一个更新(触发一个`change`事件)
foldablesFeat.foldSize = 20;

// 通过赋值改变多个值,结果是一次更新
Object.assign(foldablesFeat, { foldSize: 50, spanning: "none"});

// 在一个范围内更改多个值,导致一次更新
(function() { foldablesFeat.foldSize = 100; foldablesFeat = "single-fold-horizontal" })();

有关于spanning更详细的使用,可以查看Github上的相关教程

使用JavaScript的窗口段(Window Segments)枚举API

当使用像 Canvas 2D或WebGL这样的非DOM目标时,你可以使用新的窗口段(Window Segments)枚举API获得每个显示区域的几何图形。

window对象提供了一个getWindowSegments()方法,它将返回一个包含一个或多个DOMRects的数组,表示每个显示区域的几何形状和位置。

返回的数组是调用该方法时显区域状态的不可变快照。如果用户从跨越状态转换为非跨越状态,或旋转设备,之前检索的窗口段将无效。

const segments = window.getWindowSegments();

// PC机,传统的触摸屏幕设备,可折叠设备没有跨越状态
console.log(segments.length) //  » 1

const segments = window.getWindowSegments();

// 双屏幕或可折叠屏幕
console.log(segments.length) //  » 2

页面应该监听窗口的resizeorientationchange事件,以检测浏览器是否被调整大小,或设备是否被旋转以及检索更新的显示区域。

let segments = window.getWindowSegments();

// 跨越两上逻辑显示区域和折叠边界,并且是垂直方向的
console.log(segments.length); // » 2

// 用户决定旋转设备,浏览器仍然是跨越的,但折叠现在是水平的
// 在窗口调整中,当用户进入或离开跨越状态时,resize和orientationchange事件都会触发
window.addEventListener('resize', () => {
    // 当水平折叠时,我们最初检索的片段不再使用表示片段2的最新信息进行更新
    segments = window.getWindowSegments();
});

到目前为止,并没有明确的方法来解析折叠是垂直的(single-fold-vertical)还是水平的(single-fold-horizontal),因为这些信息可以很容易地从返回的DOMRects中计算出来:

function isSingleFoldHorizontal() {
    const segments = window.getWindowSegments();

    // 单折叠式是指设备有1折叠式和2个逻辑显示区域
    if( segments.length !== 2 ) {
        return false;
    }

    // 水平折叠single-fold-horizontal是指第一段顶部小于第二段顶部
    if( segments[0].top < segments[1].top ) {
        return true;
    }

    // 如果这个条件满足,那么折叠就是垂直的single-fold-vertical
    return false;
}

对于折叠宽度(fold-width)同样适用,Web开发人员可以使用getWindowSegments()提供的信息一了解窗口管理器是否屏蔽了在折叠后呈现的内容,以及折叠宽度(fold-width)是否大于0px

function foldWidth() {
    const segments = window.getWindowSegments();

    // 如果有1段(segment),那么折叠宽度(fold-width)不适用,返回0
    // 如果有超过2段(segment),那么我们不处理这种设备,但返回0
    if( segments.length !== 2 ) {
        return 0;
    }

    // 折叠是垂直的 (spanning: single-fold-horizontal)
    // 设备看起来像这样的: [][]
    if( segments[0].top === segments[1].top ) {
        return segments[1].left - segments[0].right;
    }

    // 如果我们达到这一点,那么折叠是水平的(spanning: single-fold-vertical)
    return segments[1].top - segments[0].bottom;
}

简单地示例

来看一个非常简单的示例:

<!-- HTML -->
<div class="content">
    <div class="blue">1</div>
    <div class="yellow">2</div>
    <div class="pink">3</div>
    <div class="green">4</div>
    <div class="fold angled stripes"></div>
</div>

先来看普通屏幕下的CSS代码:

*::after,
*::before {
    margin: 0;
    padding: 0;
    box-sizing: inherit;
}
body {
    width:  100vw;
    height: 100vh;
    overflow: hidden;
}
.text {
    font-weight: bold;
    color: white;
    margin-top: 12px;
}
.stripes {
    height: 250px;
    width: 200px;
    background-size: 40px 40px;
}
.angled {
    background-color: #737373;
    background-image:
    linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
    transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
    transparent 75%, transparent);
}
.fold {
    height: 0;
    width: 0;
}

.blue {
    height: 100px;
    width: 100px;
    background-color: blue;
    text-align: center;
    color: white;
}

.yellow {
    height: 100px;
    width: 100px;
    background-color: yellow;
    text-align: center;
}

.pink {
    height: 100px;
    width: 100px;
    background-color: pink;
    text-align: center;
}

.green {
    height: 100px;
    width: 100px;
    background-color: green;
    color: white;
    text-align: center;
}

你看看到的效果将会是这样的:

再给screen-spanning取值为single-fold-vertical的样式:

@media (screen-spanning: single-fold-vertical) {
    .fold {
        height: env(fold-height);
        width: env(fold-width);
        left: env(fold-left);
        top: 0;
        position: absolute;
    }
    .content {
        flex-direction: row;
    }

    .blue {
        height: 100px;
        width: 100px;
        position: absolute;
        left: calc(env(fold-left) - 100px);
        top: 0;
        background-color: blue;
        text-align: center;
    }

    .yellow {
        height: 100px;
        width: calc(100vw - env(fold-left) - env(fold-width));  /*fold-right*/
        position: absolute;
        left: calc(env(fold-left) + env(fold-width)); /*fold-right*/
        top: 0;
        background-color: yellow;
        text-align: center;
    }

    .pink {
        height: 100px;
        width: env(fold-left);
        position: absolute;
        left: 0;
        bottom: 0;
        background-color: pink;
        text-align: center;
    }

    .green {
        height: 100px;
        width: 100px;
        position: absolute;
        left: calc(env(fold-left) + env(fold-width)); /*fold-width*/
        bottom: 0;
        background-color: green;
        text-align: center;
    }
}

这个时候看到的效果如下:

最后给screen-spanning取值为single-fold-horizontal的样式:

@media (screen-spanning: single-fold-horizontal) {
    .fold {
        height: env(fold-height);
        width: env(fold-width);
        left: 0;
        top: env(fold-top);
        position: absolute;
    }
    .content {
        flex-direction: column-reverse;
    }

    .pink {
        height: 100px;
        width: calc(env(fold-width)/2);
        position: absolute;
        top: env(fold-bottom);
        background-color: ping;
        text-align: center;
        
    }
    .yellow {
        height: 100px;
        width: env(fold-right);
        position: absolute;
        left: 0;
        bottom: env(fold-top);
        text-align: center;
    }
    .green {
        height: 100px;
        width: calc(env(fold-width)/2);
        position: absolute;
        right: calc(env(fold-width) - 100vw);
        bottom: 0;
        text-align: center;
    }
}

效果如下:

是不是和你想象的差不多呢?

同样的,也可以使用相关的JavaScript API,将需要的参数打印出来:

<script>
    function printWindowSegments() {
        const screens = window.getWindowSegments();
        console.log("Window segments:");
        console.table(screens);
        console.log("Window size:", window.innerWidth, window.innerHeight);
    }

    function isSingleFoldHorizontal() {
        const screens = window.getWindowSegments();

        if( screens.length !== 2 ) {
            console.log("Window segments lengt:");
            console.log(screens.length);
            return false;
        }

        // 水平折叠single-fold-horizontal是指第一段顶部小于第二段顶部
        if( screens[0].top < screens[1].top ) {
            console.log(screens[0].top)
            console.log(screens[1].top)
            console.log('single-fold-horizontal')
            return true;
        }

        // 如果这个条件满足,那么折叠就是垂直的single-fold-vertical
        console.log('single-fold-vertical')
        return false;
    }

    function foldWidth() {
        const screens = window.getWindowSegments();

        // 如果有1段(segment),那么折叠宽度(fold-width)不适用,返回0
        // 如果有超过2段(segment),那么我们不处理这种设备,但返回0
        if( screens.length !== 2 ) {
            console.log('fold-width:', 0)
            return 0;
        }

        // 折叠是垂直的 (spanning: single-fold-horizontal)
        // 设备看起来像这样的: [][]
        if( screens[0].top === screens[1].top ) {
            console.log('fold-width:', screens[1].left - screens[0].right)
            return screens[1].left - screens[0].right;
        }

        // 如果我们达到这一点,那么折叠是水平的(spanning: single-fold-vertical)
        console.log('fold-width:', screens[1].top - screens[0].bottom )
        return screens[1].top - screens[0].bottom;
    }

    printWindowSegments();
    isSingleFoldHorizontal();
    foldWidth();

    window.addEventListener("resize", () => {
        console.log("resized");
        printWindowSegments();
        isSingleFoldHorizontal();
        foldWidth();
    });
</script>

简单地说,按照上面的套路,我们就可以轻易的实现多屏幕和可折叠屏幕的适配。

设想未来

我们上面讨论的都是双屏幕或可折叠屏幕,但也有一些设备厂商提出一些更超前的概念,在将来可能还会有更多的屏幕:

也就是说,在未来的设备可能会有三个屏幕,两个折叠边界,三个逻辑显示区域:

甚至还会有更多。

作为Web开发人员,我们不能仅为当下考虑,我们更应该为将来做计划。换句话说,我们提供的技术方案不能仅局限于当下,还需要为未来做考虑。

回到技术方案上来说,JavaScript和CSS不同,因为JavaScript有数组、循环和条件等特性,这使用窗口段枚举API和具有N个逻辑显示区域和折叠边界可以直接的映射。比如上面提到的概念机,当浏览器是横跨三个逻辑显示区域,两个可折叠边界区域时,我们调用getWindowSegments()方法将返回一个具有三个DOMRects的数组。这样我们就可以像上面的示例一样,为它们提供相应的适配解决方案。

小结

可折叠Web的出现,让移动优先的设计变得更加复杂,但也更加令人兴奋。可折叠Web可能是第一次手持设备感到空间的扩展而不是限制。对于一些Web应用或Web页面来说,需要做一定的调整,而对于另一些Web应用来说,则意味着需要大规模的重新设计。这个范围取决于开发人员的创新。

换句话说,前路漫漫,一切都将是未知,但只有我们不断的进取,才能让它们离我们更近,为未来不同的设备终端提供最适合的解决方案。