往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)
✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✏️ 记录一场鸿蒙开发岗位面试经历~
✏️ 持续更新中……
概述
折叠屏产品
- 阔折叠:Pura X系列。
- 大折叠:Mate X系列。
- 小折叠:Pocket系列。
- 三折叠:Mate XT系列。
折叠状态
- 展开态:折叠屏设备完全展开后的形态。有更大的屏幕尺寸,可充分显示应用内容。
- 折叠态:折叠屏设备折叠后的形态。折叠后屏幕尺寸变小。
- 悬停态:折叠屏设备处于完全展开和折叠的中间状态,可平稳放置。
体验设计点
折叠屏相对于普通手机有一个明显的特点:可随时折叠展开、折叠展开会导致屏幕属性改变。
为了能够充分利用折叠屏的特点,提供良好的使用体验,折叠屏UX设计中需考虑如下体验诉求:
- 体验连续
屏幕可随时折叠展开,在体验上要保证用户体验的连续性,应用需要遵从屏幕显示的兼容和应用状态的连续。- 屏幕兼容:由于屏幕尺寸发生变化,应用应采用适当的手段对屏幕上内容布局进行优化调整。
- 应用连续:应用在折叠与展开状态切换的过程中保持正常运行。
- 体验增值
屏幕尺寸变大后,用户体验在某些方面有增值:- 更多内容:大尺寸屏幕可显示更多内容,提高显示和操作的效率。
- 更加沉浸:大尺寸屏幕可显示更清晰的细节,适合图片浏览、视频欣赏、游戏等应用和场景。
- 多任务并行:多个窗口可同时在大尺寸屏幕内展示,为用户多任务并行提供了直观高效的方式。
折叠屏UX体验标准
本标准从影响用户体验的各个维度定义了相应测试规范,规定了应用需达到的基础体验要求,用于引导应用的设计与开发,以保证应用良好的使用体验。
折叠屏需要遵循的UX体验标准包括:
开合连续性
开合过程包含折叠状态和展开状态下的两种页面布局,通过显示窗口的属性(宽度和高宽比)决定开合过程前后的页面布局。系统侧设计了横向和纵向断点分别代表窗口宽度和窗口高宽比,页面布局的一多要求使用横向和纵向断点进行判断实现。
说明
折叠屏页面布局的判断条件不推荐使用以下接口,否则可能带来不同屏幕尺寸折叠屏下的布局混乱问题。
// 1. 设备类型为phone,且支持可折叠
if (deviceInfo.deviceType === 'phone' && display.isFoldable()) {}
// 2. 判断当前折叠状态是否为展开态/折叠态/半折叠态
if (display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_EXPANDED) {}
除页面布局的判断不能使用deviceType/isFoldable/getFoldStatus外,应用的其他逻辑可以使用 display 的开合能力来满足开合场景下不同需求。常见display API如下:
API | 说明 |
---|---|
display.isFoldable | 检查设备是否可折叠 |
display.getCurrentFoldCreaseRegion | 在当前模式下获取折叠折痕区域 |
display.getFoldStatus | 获取可折叠设备的当前折叠状态 |
display.on(‘foldStatusChange’) | 开启折叠设备折叠状态变化的监听 |
display.off(‘foldStatusChange’) | 关闭折叠设备折叠状态变化的监听 |
display.on(‘foldDisplayModeChange’) | 开启折叠设备屏幕显示模式变化的监听 |
display.off(‘foldDisplayModeChange’) | 关闭折叠设备屏幕显示模式变化的监听 |
需要注意的是,若需要获取折叠屏的状态变化后,屏幕的宽高等信息,需要使用display.on(‘foldDisplayModeChange’)接口进行监听。如果使用display.on(‘foldStatusChange’)监听,将出现因接口调用时序问题导致的获取宽高错误问题。示例代码如下:
display.on('foldDisplayModeChange', (data: display.FoldDisplayMode) => {
let displayInfo: display.Display = display.getDefaultDisplaySync();
if (data === display.FoldDisplayMode.FOLD_DISPLAY_MODE_FULL) {
console.info('当前屏幕状态:全屏显示');
console.info('屏幕宽度: ' + displayInfo.width);
console.info('屏幕高度: ' + displayInfo.height);
} else if (data === display.FoldDisplayMode.FOLD_DISPLAY_MODE_MAIN) {
console.info('当前屏幕状态:主屏幕显示');
console.info('屏幕宽度: ' + displayInfo.width);
console.info('屏幕高度: ' + displayInfo.height);
}
});
开合流畅
简介 | 描述 |
---|---|
标准描述 | 折叠屏折叠展开时,变化过程有连续动效,而不是硬切换。 |
测试方法 | 对折叠屏进行折叠展开操作,观察界面动效。 |
判定标准 | 在折叠屏折叠展开过程中,界面动效自然流畅,无卡顿等异常。 |
标准等级 | 推荐 |
适用设备类型 | 折叠屏 |
需考虑的特殊事项 | 无 |
悬停适配
折痕避让
横竖屏适配
多窗适配
上下图文信息量适中
折叠屏需要同时参考通用应用、大屏应用、折叠屏应用的UX体验标准。
折叠屏应用开发指导
随着终端设备形态日益多样化,应用设计需要考虑界面能否适配不同的屏幕尺寸、屏幕方向和设备类型。同时还需要保持多设备体验的连续性,改善多端独立的设计、尽可能降低开发者的工作量和维护成本。基于此 HarmonyOS 为设计师提供了面向多设备的设计指导,让设计师在进行多端设计时有一套科学的方法,最大程度减少设计的工作量,保障多端设计在一定程度的一致性。同时 HarmonyOS 也提供了对应的技术能力,帮助开发者快速地进行多端应用设计。
本指导结合用户在多端设备上的历史交互习惯、各场景下的使用诉求等,进行了一些设计方法的总结,主要包括如下几个部分:
- 基础要求:在多设备应用设计中需要遵守的基础体验要求,如果不满足基础要求,则会极大损害用户的使用体验。
- 响应式布局:在宽屏设备上,针对部分常见的界面元素提出了响应式布局设计范式,以避免简单拉伸、放大等导致的一些体验问题。
- 增值体验:在多设备应用设计中可以考虑更多体验上的变化,在合适的场景下提供增值的体验。
- 场景化设计参考:针对具体垂类场景应用给出了场景内典型页面的设计建议,方便设计师进行更有针对性的参考和选用。
基本要求
折叠屏应用开发具有导航、横竖屏、挖孔、多窗、弹出框等基础要求。
导航适配
1.底部导航&侧边导航
通常手机和大折叠使用底部页签导航;平板及其他宽屏设备使用侧边页签或侧边栏导航。调用系统提供的控件可自动适配该导航规则。
2.底部导航条适配
移动端设备应用内的底部页签、底部工具栏、底部文本,或底部悬浮按钮需要自动抬高避让底部导航条。应用内的可滚动内容,可直接显示在导航条下方,仅当滚动到内容的最底部时需要向上抬高避免被底部导航条遮挡。调用控件,可自动实现以上导航条的避让规格。
说明
沉浸式页面,例如全屏播放视频、游戏、阅读等场景下,超过 2 秒自动隐藏底部导航条,从底部上滑或从顶部下滑可触发恢复显示底部导航条。
横竖屏与挖孔适配
手机和大折叠折叠态的应用通常需要适配竖屏布局。仅部分特殊场景例如横屏游戏、长视频需要适配横屏布局。
大折叠展开态、平板上,应用需要同时适配横屏和竖屏布局。
当设备尺寸比例接近 1:1 时,建议横竖屏使用相同或相近的布局。
当设备尺寸比例差异大时,横竖屏可以使用不同的布局,从而提供更好的使用体验,例如横屏后自动分栏或横屏后自动露出右侧的辅助信息等。
横竖屏适配过程中,需要考虑核心内容或重要交互不要被挖孔区遮挡,如果被遮挡则进行局部内容等避让;可滚动内容或非核心信息无需专门避让挖孔区;要避免因为避让挖孔导致左右侧留白不一致。
1.横竖屏适配
实现方案
设备场景 | 是否需要支持横竖屏旋转 |
---|---|
手机(直板机) | 由应用决定是否需要支持,默认不支持 |
小折叠(Pocket系列) | 内屏(展开状态):同直板机手机 |
大折叠(Mate X系列) | 内屏(展开状态):类方屏需要支持 外屏(折叠状态):同直板机手机 |
三折叠(Mate XT系列) | F态(单屏显示):同直板机手机 M态(双屏显示):类方屏需要支持 G态(三屏显示):需要支持 |
阔折叠(Pura X系列) | 内屏(展开状态):类方屏需要支持 外屏(折叠状态):同直板机手机 |
多窗适配
在手机和折叠屏折叠态、展开态,应用需要适配竖向悬浮窗、上下分屏、左右分屏、分屏比例支持自由调节。仅部分特殊场景例如横屏游戏、长视频,需要适配横向悬浮窗。
平板及更宽的设备上 ,应用需要适配自由窗口 (即可任意拖大、拖小、拖宽、拖窄) ,横屏时支持左右分屏,竖屏时支持上下分屏,且支持分屏比例的自由调节。
说明
在多端设备上,长视频、直播、会议、通话等场景,需要支持画中画。
弹出框适配
建议调用系统的弹出框控件,避免在宽屏设备上弹出框过宽或过高的问题。
说明
若应用自定义弹出框尺寸,则建议在 8 栅格及以上的设备上,弹出框宽度为 480vp ;宽屏设备上弹出框的物理高度不超过手机上的该弹出框高度的 1.5 倍。
键鼠适配
平板及更宽的设备上的应用需要进行基础的鼠标、键盘等适配。
页面布局
折叠屏应用的页面布局包括响应式布局与Web页面布局。
1.响应式布局
应用内的页签、搜索、入口图标、广告图、列表、卡片、图片、内容模块等可以灵活地通过形变、延伸布局、重复布局、挪移布局、宫格布局、瀑布流布局等进行宽屏设备上的响应式适配,以达到较好的信息量和舒适的浏览体验。
1.1子页签&搜索
手机和大折叠折叠态,搜索和子页签分两排显示。
大折叠展开态、平板及更宽的设备上,子页签和搜索可以同一排显示,搜索框根据子页签数量的多少而自适应调整长度。
1.2入口图标
在多端设备上,建议 1 行显示的入口图标数量限制在一定范围内。例如在大折叠和平板竖屏上一排图标不超过 8 个,在平板横屏上一排图标不超过 13 个,要避免一排图标过多导致信息过密。
1.3广告图
1.3.1卡片广告
在宽屏设备上建议使用延伸布局和形变进行卡片广告的响应式适配。
例如手机上一张广告卡片,在折叠屏可显示两张广告卡片,在平板显示三张广告卡片。同时基于各端的物理尺寸可进行广告卡片的自适应形变,在宽屏设备上广告卡片更长,在窄屏设备上广告卡片更高。
1.3.2沉浸广告
音视频等应用,为提供更沉浸的影音娱乐体验,可使用沉浸广告图效果。
应用使用沉浸广告时,建议沉浸广告的背景和广告内容元素分为多个图层,且为宽屏设备提供更长的背景图,或通过智能裁剪为不同宽度的设备裁剪出合适高度的背景图。保持背景图横向铺满整个设备宽度,且要避免背景图过高。广告的图片、文本等内容元素在宽屏设备上使用响应式布局。
金融理财、生活服务等类型的应用也经常使用沉浸广告图打造更好的营销氛围。需要考虑为多端提供一个适宜的沉浸广告高度,避免在宽屏设备上等比放大导致整个广告过高,导致首屏的信息量过少。
折叠屏和平板横屏时,建议沉浸广告图不超过 0.5 倍的屏幕高度,平板竖屏时建议沉浸广告图不超过 0.4 倍的屏幕高度。
1.4列表
1.4.1列表的重复布局
在宽屏设备上,为避免结构过于单调且信息量过少,可通过重复布局来改善页面的浏览舒适度和使用效率。
1.4.2列表的挪移布局
列表结构中的大视频或单张大图片,在宽屏设备上也可以通过图文挪移布局来避免视频或图片显示过大。
1.4.3多列瀑布流
可以通过从单列瀑布流到多列瀑布流的布局变化,带来宽屏设备上更舒适的增值阅读体验。
1.5卡片
手机上的单列的卡片,在宽屏设备上,通过分栏 + 瀑布流/宫格布局的方式进行显示适配,提升浏览效率且提供舒适布局。
1.6详情大图
在宽屏设备上,页面内的大图可以通过延伸布局或挪移布局进行适配。
1.7内容模块
同一个设备上,也可以根据多个模块的内容尺寸进行自适应的挪移布局。
2.Web页面布局
Web页面布局中讲述文字、图片、宫格元素、弹窗、广告、轮播图等的适配问题及实现方案。
2.1大折叠文字大小适配
展开态字体大小与折叠态一致。
两种实现方案
- 使用@media属性设置字体大小。
- 使用rem设置字体大小。
参考代码
- 使用@media属性单独设置大折叠展开态时的字体大小。
span {
font-size: 4.6vw;
}
/* 使用@media属性设置字体大小 */
@media only screen and (min-width: 500px) {
span {
font-size: 2.3vw;
}
}
<span>xxxxxxxxxx</span>
- 字体大小跟随屏幕宽度变化的场景,可以使用rem实现,并控制字体大小的变化范围。
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
span {
font-size: 1rem;
}
</style>
<script>
/* 示例代码,根据屏幕宽度设置字体大小 */
function setHtmlFontSize() {
var baseFontSize = 16;
var baseScreenWidth = 375;
var screenWidth = window.innerWidth;
var screenHeight = window.innerHeight;
/* 示例代码:控制字体大小的变化范围 */
var fontSize = (Math.min (screenWidth, 400) / baseScreenWidth) * baseFontSize;
/* var fontSize = (screenWidth / baseScreenWidth) * baseFontSize; */
document.documentElement.style.fontSize = fontSize + "px";
}
window.addEventListener("load", setHtmlFontSize);
window.addEventListener("resize", setHtmlFontSize);
</script>
</head>
<body>
<div>
<span>xxxxxxxxxxxxxxxxxxxxxxx</span>
</div>
</body>
</html>
2.2大折叠展开态图片适配
图片放大导致显示信息减少。
图片大小与折叠态相似、单张图片的情景。
实现方案
通过 @media 样式,设置图片元素或其容器在大折叠展开态时的尺寸为原来的约1/2,即约为折叠态的1倍大小。
参考代码
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
div {
width: 90vw;
margin: 0 auto; /* 图片居中 */
}
/* 使用@media属性实现自适应 */
@media only screen and (min-width: 500px) {
div {
width: 50vw;
}
}
</style>
</head>
<body>
<div>
<img src=" D:\XXX.jpg" style="width: 100%" />
</div>
</body>
</html>
2.3折叠屏展开态下图标元素自适应
图标元素放大,导致页面整体显示信息减少。
图标元素尺寸不变,页面整体显示信息不变或增加。
实现方案
将重复元素的尺寸设定为绝对值、将重复元素容器的宽度设置为相对值。
参考代码
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
* {
margin: 0;
padding: 0;
}
ul {
width: 90vw;
height: auto;
margin: 0 auto;
font-size: 0;
}
li {
width: 60px;
height: 60px;
background-color: aqua;
display: inline-block;
margin: 0;
margin-bottom: 5px;
}
/* 示例:左对齐、均匀布局的一种方式 */
li:not(:nth-child(4n)) {
margin-right: calc((100% - 60px * 4) / 3);
}
/* 使用@media属性实现自适应 */
@media only screen and (min-width: 500px) {
li:not(:nth-child(8n)) {
margin-right: calc((100% - 60px * 8) / 7);
}
}
</style>
</head>
<body>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</body>
</html>
2.4大折叠展开态下宫格元素自适应
宫格元素放大,导致显示信息减少。
宫格元素两列变三列,显示内容增多。
实现方案
通过 @media 属性,展开态下应用“元素宽高减小至原1/n”的样式,达到列数增加的效果;将容器宽度固定、高度设置为auto。
参考代码
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
#container {
width: 90vw;
height: auto;
margin: 0 auto;
font-size:0;
}
img {
width:100%;
}
.item {
margin-bottom: 10px;
display: inline-block;
}
/* 使用@media属性实现自适应 */
@media only screen and (max-width: 499px){
.item {
width:49%;
}
.item:not(:nth-child(2n)) { /* 示例:左对齐、均匀布局的一种方式 */
margin-right: 2%;
}
}
/* 使用@media属性实现自适应 */
@media only screen and (min-width: 500px){
.item {
width:32%;
}
.item:not(:nth-child(3n)) {
margin-right: 2%;
}
}
</style>
</head>
<body>
<div id="container">
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
<div class="item">
<img src="D:\XXX.jpg" />
</div>
</div>
</body>
</html>
2.5大折叠展开态下弹窗元素大小自适应
弹窗元素过大。
弹窗元素尺寸与折叠态一致。
实现方案
通过 @media 样式,设置弹窗元素的容器在大折叠展开态时的相对尺寸为原来的1/2,即约为折叠态的1倍大小。
参考代码
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
/* 展示Dialog */
function showDialog() {
document.getElementById("container").style.display = "block";
}
/* 隐藏Dialog */
function hideDialog() {
document.getElementById("container").style.display = "none";
}
window.onload = showDialog;
</script>
<style>
#container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
z-index: 9999;
}
#content {
position: absolute;
width: 70vw;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
font-size: 0;
}
/* 使用@media属性实现自适应 */
@media only screen and (min-width: 500px) {
#content {
width: 35vw;
}
}
img {
width: 100%;
}
button {
display: block;
margin: 0 auto;
height: 20px;
background-color: #007bff;
color: #fff;
border: none;
font-size: 12px;
}
</style>
</head>
<body>
<div id="container" style="display: none">
<div id="content">
<img src="./image/image1.png" />
<button onclick="hideDialog()">close</button>
</div>
</div>
</body>
</html>
2.6大折叠展开态下广告图大小自适应
广告图尺寸过大,导致一页显示内容过少。
广告图适当放大后,保留核心内容、裁剪非核心内容。
实现方案
通过 @media属性和 overflow:hidden 会隐藏溢出部分的显示的特性,将广告图适当放大后,保留核心内容、裁剪非核心内容。因涉及内容裁剪,请开发者根据UX规范做具体设计。
参考代码
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
div {
width: 90vw;
height: 200px;
margin: 0 auto;
overflow: hidden;
}
img {
width: 100%;
}
/* 使用@media属性实现自适应 */
@media only screen and (min-width: 500px) {
div {
height: 300px;
}
img {
margin-top: -50px;
}
}
</style>
</head>
<body>
<div>
<img src="D:\XXX.jpg" />
</div>
</body>
</html>
2.7大折叠展开态下横向轮播的运营类图片大小自适应
运营类图片尺寸过大,导致一页显示内容过少。
弹窗元素尺寸与折叠态一致。
实现方案
使用@media属性将图片尺寸减小为原来的1/2、即与折叠态一致;将图片左移25%屏幕宽度的距离,使之居中显示;使用border-right等方法在两张图片之间加入间距。
建议对屏幕尺寸变化事件("resize"事件)进行监听,在开合动作后重新设置位移,以保持内容显示连续性。
参考代码
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
#par {
width: 90vw;
height: auto;
overflow: hidden;
font-size: 0;
margin: 0 auto;
}
#container {
width: 100%;
display: inline-block;
white-space: nowrap;
}
.item {
width: 100%;
}
/* 展开态 0.5+1+0.5 布局示例 */
@media only screen and (min-width: 500px) {
#container {
margin-left: 25%;
}
.item {
width: 50%;
border-right: 10px solid transparent;
}
}
</style>
</head>
<body>
<div id="par">
<div id="container" style="transform: translateX(0)">
<img class="item" src="./images.jpg" />
<img class="item" src="./images3.jpg" />
<img class="item" src="./images.jpg" />
<img class="item" src="./images3.jpg" />
<img class="item" src="./images.jpg" />
<img class="item" src="./images3.jpg" />
</div>
</div>
<script>
let par = document.getElementById("par");
let container = document.getElementById("container");
let itemWidth = document.getElementsByClassName("item")[0].offsetWidth;
let itemNum = document.getElementsByClassName("item").length;
let initialX = 0;
let distanceX = 0;
let imgFlag = 0;
par.addEventListener("touchstart", function (event) {
initialX = event.touches[0].clientX;
});
par.addEventListener("touchmove", function (event) {
event.preventDefault();
distanceX = event.touches[0].clientX - initialX;
transform = -(imgFlag * itemWidth) + distanceX;
container.style.transform = "translateX(" + transform + "px)";
});
par.addEventListener("touchend", function (event) {
if (distanceX < -30) {
imgFlag = imgFlag < itemNum - 1 ? imgFlag + 1 : 0;
} else if (distanceX > 30) {
imgFlag = imgFlag > 0 ? imgFlag - 1 : itemNum - 1;
}
container.style.transform =
"translateX(" + -(imgFlag * itemWidth) + "px)";
});
// 保持折叠展开的内容显示连续性
window.addEventListener("resize", function () {
itemWidth = document.getElementsByClassName("item")[0].offsetWidth;
container.style.transform =
"translateX(" + -(imgFlag * itemWidth) + "px)";
});
</script>
</body>
</html>
常见问题
展开态使用0.5+1+0.5三张图布局时,首图和尾图两侧有留白。
实现方案
在图片列表前、后各补充一张图片,对留白部分进行填充;通过控制图片序号的循环范围,使填充图不显示在画面中央。
参考代码
修改上述代码中的JS部分:
<script>
let par = document.getElementById("par");
let container = document.getElementById("container");
let itemWidth = document.getElementsByClassName("item")[0].offsetWidth;
let itemNum = document.getElementsByClassName("item").length;
let initialX = 0;
let distanceX = 0;
let imgFlag = 1; // 记录当前显示的图片序号,因填充图不显示而从1开始记录
FillImg(); // 首尾填充两张图,使图片两边不出现留白
container.style.transform =
"translateX(" + -(imgFlag * itemWidth) + "px)"; // 从非填充的图片开始显示
/*
* 下述为滑动切换图片的一种示例;
* 需注意第一张图(序号 0 )和最后一张图(序号 itemNum-1 )是复制出来的填充图,不显示在画面中央
*/
par.addEventListener("touchstart", function (event) {
initialX = event.touches[0].clientX;
});
par.addEventListener("touchmove", function (event) {
event.preventDefault();
distanceX = event.touches[0].clientX - initialX;
transform = -(imgFlag * itemWidth) + distanceX;
container.style.transform = "translateX(" + transform + "px)";
});
par.addEventListener("touchend", function (event) {
if (distanceX < -30) {
imgFlag = imgFlag < itemNum - 2 ? imgFlag + 1 : 1;
} else if (distanceX > 30) {
imgFlag = imgFlag > 1 ? imgFlag - 1 : itemNum - 2;
}
container.style.transform =
"translateX(" + -(imgFlag * itemWidth) + "px)";
});
function FillImg() {
let newImg = document
.getElementsByClassName("item")
[itemNum - 1].cloneNode();
container.insertBefore(
newImg,
document.getElementsByClassName("item")[0]
);
newImg = document.getElementsByClassName("item")[1].cloneNode();
container.appendChild(newImg);
itemNum += 2; // 图片总数+2;
}
// 保持折叠展开的内容显示连续性
window.addEventListener("resize", function () {
itemWidth = document.getElementsByClassName("item")[0].offsetWidth;
container.style.transform =
"translateX(" + -(imgFlag * itemWidth) + "px)";
});
</script>
折叠屏悬停
折叠屏悬停描述折叠屏中的折痕避让和悬停适配,下文以大折叠为例介绍折叠屏悬停的一些典型场景。
1.悬停适配与折痕避让
折叠屏产品具有独特的悬停态,即用户可以将产品半折后立在桌面上,实现免手持的体验。悬停态场景非常适合不需要频繁进行交互的任务,例如视频通话、播放视频、拍照、听歌等。
说明
悬停态时,中间弯折区域难以操作且显示内容会变形,因此建议页面内容进行折痕区避让适配。建议上半屏内容由中线向上下进行避让,避让距离从 getCurrentFoldCreaseRegion API获取。
悬停态若触发应用内的弹出框、半模态等操作型控件,建议交互型控件在下半屏显示;若触发跟随上下文的控件,例如菜单等,建议跟随触发元素的位置显示。控件高度需要根据悬停态的屏幕尺寸进行最大高度的适配。
折痕避让
实现方案
系统提供的FolderStack组件已经实现了折痕自动避让,如果需要实现自定义布局,需要获取折痕区域进行布局避让。获取折痕区域可以使用 getCurrentFoldCreaseRegion API。
import display from '@ohos.display';
try {
display.getCurrentFoldCreaseRegion();
} catch (exception) {
console.error('Failed to obtain the current fold crease region. Code: ' + JSON.stringify(exception));
}
参考代码
import display from '@ohos.display';
import { Callback } from '@ohos.base';
@Entry
@Component
export struct Crease {
@State status: string = "1"
// 启动就注册监听
aboutToAppear() {
let callback: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
console.info('Listening enabled. Data: ' + JSON.stringify(data));
// 获取折叠折痕区域,left与top属性为矩形区域的左边界与上边界,width与height属性为矩形区域的宽高。
this.status = data.toString() + " " + display.getCurrentFoldCreaseRegion().creaseRects[0].left + " " + display.getCurrentFoldCreaseRegion().creaseRects[0].top
+ " " + display.getCurrentFoldCreaseRegion().creaseRects[0].width + " " + display.getCurrentFoldCreaseRegion().creaseRects[0].height;
};
try {
display.on('foldStatusChange', callback);
} catch (exception) {
console.error('Failed to register callback. Code: ' + JSON.stringify(exception));
}
}
build() {
Column() {
Text(this.status).height(50).width("100%").textAlign(TextAlign.Center).fontSize(25).backgroundColor(Color.Red)
}
.height("100%")
.width("100%")
.borderWidth(1)
.backgroundColor(Color.Pink)
.justifyContent(FlexAlign.Start)
}
}
悬停适配
实现方案
悬停适配推荐使用 FolderStack 组件,FolderStack继承于Stack(层叠布局)控件,具有折叠屏悬停能力,通过识别upperItems自动避让折叠屏折痕区后移到上半屏。
FolderStack(value?: { upperItems?: Array<string>})
参考代码
@Entry
@Component
export struct Folder {
@State len_wid: number = 480
@State w: string = "40%"
build() {
Column() {
// upperItems将所需要的悬停到上半屏的id放入upperItems传入,其余组件会堆叠在下半屏区域
FolderStack({ upperItems: ["upperitemsId"] }) {
// 此Column会自动上移到上半屏
Column() {
Text("video zone").height("100%").width("100%").textAlign(TextAlign.Center).fontSize(25)
}.backgroundColor(Color.Pink).width("100%").height("100%").id("upperitemsId")
// 下列两个Column堆叠在下半屏区域
Column() {
Text("video title")
.width("100%")
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Red)
.fontSize(25)
}.width("100%").height("100%").justifyContent(FlexAlign.Start)
Column() {
Text("video bar")
.width("100%")
.height(50)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Red)
.fontSize(25)
}.width("100%").height("100%").justifyContent(FlexAlign.End)
}
.backgroundColor(Color.Yellow)
// 是否启动动效
.enableAnimation(true)
// 是否自动旋转
.autoHalfFold(true)
// folderStack回调 当折叠状态改变时回调
.onFolderStateChange((msg) => {
if (msg.foldStatus === FoldStatus.FOLD_STATUS_EXPANDED) {
console.info("The device is currently in the expanded state")
} else if (msg.foldStatus === FoldStatus.FOLD_STATUS_HALF_FOLDED) {
console.info("The device is currently in the half folded state")
} else {
// .............
}
})
// folderStack如果不撑满页面全屏,作为普通Stack使用
.alignContent(Alignment.Bottom)
.height("100%")
.width("100%")
.borderWidth(1)
.backgroundColor(Color.Yellow)
}
.height("100%")
.width("100%")
.borderWidth(1)
.backgroundColor(Color.Pink)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
}
2.典型案例
悬停状态下,界面布局应自动调整。即将浏览型内容上移,在上半屏显示;将操作类控件元素下沉,在下半屏显示。提供更舒适和高效的使用体验。
视频
悬停时,上半屏显示视频画面,下半屏显示视频相关的操作按钮或周边信息。
视频通话
悬停时,上半屏显示通话界面,下半屏显示通话相关的操作按钮。
短视频
短视频悬停时,头像、评论、视频画面等显示的内容在上半屏展示,输入框等操作在下半屏显示。短视频类应用进行需要手势操作快速切换视频内容。建议下半屏支持横向滑动切换视频。
健身视频
悬停时,上半屏显示动作跟练视频内容、进度,下半屏显示播放及切换功能。
拍摄
悬停时,上半屏显示取景画面,下半屏显示取景模式、拍摄参数控制按钮等操作类功能。
音频播放
悬停时,上半屏显示专辑封面或歌词、音乐MV,下半屏显示音频播控功能。
增值体验
增值体验中着重描述体验与交互:
- 高效体验:分栏、应用内分屏、缩放。
- 沉浸体验:沉浸广告、沉浸浏览、沉浸观影。
- 轻交互:浅层窗口、临时双窗、长按预览。
- 跟手交互:跟手弹框、跟手输入。
1.高效体验
1.1分栏
8 栅格以上的设备可支持分栏,适用的设备包括手机横屏、折叠屏、平板及更宽的设备上等。
办公类、效率型、IM 社交类应用适用于分栏布局,例如系统应用中的邮件、日历、备忘录、文件管理、设置、短信、畅连、联系人等。金融类、电商购物类部分页面,也可以通过分栏提升宽屏上的使用效率。
分栏布局时,允许应用内通过点击“全屏”按钮,从分栏切换至临时全屏或点击“缩小”按钮,从临时全屏恢复分栏。
实现方案
可使用 Navigation 组件实现分栏。Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示( NavDestination 的子组件),首页和非首页通过路由进行切换。
参考代码
@Entry
@Component
export struct NavigationComponent {
@State TooTmp: ToolbarItem = {'value': "func", 'action': ()=> {}}
private arr: number[] = [1, 2, 3];
build() {
Column() {
// 路由导航的根视图容器
Navigation() {
List({ space: 12 }) {
ForEach(this.arr, (item:string) => {
ListItem() {
// 导航组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。
NavRouter() {
Text("NavRouter" + item)
.width("100%")
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
// 非首页显示内容
NavDestination() {
Text("NavDestinationContent" + item)
}
.title("NavDestinationTitle" + item)
}
}
}, (item:string):string => item)
}
.width("90%")
.margin({ top: 12 })
}
.title("主标题")
.mode(NavigationMode.Auto)
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5')
}
}
1.2应用内分屏
文档编辑、阅读、购物、IM 对话、通话等场景,通常需要进行多个内容对比或多个任务协同。
此类场景下,建议通过页面内的“分屏”按钮触发任务分屏。形成分屏后,“分屏”按钮自动切换为“全屏”按钮,点击“全屏”按钮,即退出另一侧的分屏任务,让当前焦点所在的任务回到全屏。
1.3缩放
瀑布流或宫格布局,建议支持通过双指缩放进行布局列数的调整,从而满足效率型和大图浏览型的不同用户诉求。
新闻详情、网页详情、笔记、文档等图文阅读类页面,建议支持通过双指缩放调整文字大小。
实现方案
双指缩放属于 交互归一 的一种基础输入方式,交互归一后开发者就无需关注当前设备的输入方式,只需要在交互归一接口中做逻辑实现即可。双指缩放使用 PinchGesture API实现。PinchGesture可用于触发捏合手势,触发捏合手势的最少手指为2指,最大为5指。
PinchGesture(value?: { fingers?: number, distance?: number })
参考代码
双指缩放图片:
@Entry
@Component
export struct PinchImage {
list: string[] = ['image1','image2','image3','image4','image5','image6']
@State GridColumn: string = '1fr 1fr 1fr'
@State GridRow: string = '1fr 1fr'
build() {
Column(){
Grid() {
ForEach(this.list, (item: string) => {
GridItem() {
Text(item)
}.backgroundColor(Color.Grey)
})
}
.columnsTemplate(this.GridColumn)
.rowsTemplate(this.GridRow)
.rowsGap(12)
.columnsGap(12)
}
// 触发双指缩放时,改变Grid的布局
.gesture(PinchGesture({fingers:2}).onActionUpdate((event:GestureEvent)=>{
if (event.scale>1) {
// 增加动画效果
animateTo({
duration: 500
}, () => {
// 双指缩放比例>1时,栅格Grid列数变为2列
this.GridColumn = '1fr 1fr';
})
}else {
animateTo({
duration: 500
}, () => {
// 双指缩放比例<=1时,栅格Grid列数变为3列
this.GridColumn = '1fr 1fr 1fr';
})
}
}))
}
}
2.沉浸体验
广告、浏览、观影方面的沉浸体验。
2.1沉浸广告
音视频等应用,为提供更沉浸的影音娱乐体验,或电商购物、金融理财、生活服务等应用为营造营销氛围感,可使用沉浸广告图效果。使用沉浸广告图时,广告图的背景和广告内容元素需要分层,并根据设备的屏幕宽度进行自适应布局。
实现方案
广告图的背景使用 backgroundImage API实现,并使用Row组件显示广告内容文字占位。
2.2沉浸浏览
新闻阅读、社交资讯、生活服务、电商、办公等类型的内容,在详情页浏览内容时,可以通过上滑隐藏标题栏、工具栏,下滑或停留超过一定时长恢复显示标题栏、工具栏的方式,提供更沉浸的浏览体验。
实现方案
沉浸浏览使用到滚动事件,因此可以在滚动的开始与结束期间隐藏或者展示标题栏、工具栏。以顶部标题栏和底部工具栏的barHeight初始高度56vp为例,barOpacity初始透明度为1。调用Scroll、List和WaterFlow组件的onScrollFrameBegin接口,在滑动过程中,根据当前Y轴滑动的偏移量,逐渐减少标题栏和工具栏的高度和透明度,实现滑动过程隐藏的效果。调用onScrollStart接口,在滑动开始时重置当前Y轴的偏移量。调用onScrollStop接口,在手指离开屏幕且滑动停止时,2秒后使用动画将高度和透明度复原。
参考代码
export struct ScrollTest {
// 固定区的高度
@State barHeight: number = 56;
// 固定区的透明度
@State barOpacity: number = 1;
// 当前Y轴滑动偏移量
@State currentYOffset: number = 0;
build() {
List() {
// ...
}
.onScrollFrameBegin((offset: number, state: ScrollState) => {
this.currentYOffset += Math.abs(offset);
// 以Y轴偏移量100为例,偏移量小于100时逐渐隐藏固定区
if (this.currentYOffset <= 100) {
this.barHeight = 56 * (1 - this.currentYOffset / 100);
this.barOpacity = 1 - this.currentYOffset / 100;
}
// 偏移量大于100时直接隐藏固定区
else {
this.barHeight = 0;
this.barOpacity = 0;
}
return { offsetRemain: offset };
})
.onScrollStart(() => {
// 滑动开始时重置Y轴滑动的偏移量
this.currentYOffset = 0;
})
.onScrollStop(() => {
// 滑动停止时使用动画将固定区的高度和透明度复原
setTimeout(() => {
animateTo({
duration: 300
}, () => {
this.barHeight = 56;
this.barOpacity = 1;
})
}, 2000);
})
}
}
2.3沉浸观影
全屏播放视频时,建议提供以下两种体验:
- 减少屏幕旋转。例如在接近方形的折叠屏上,点击全屏播放按钮时,需要避免屏幕旋转导致的观影体验中断。
- 减少弹幕对视频内容的遮挡。例如当屏幕内有黑边时,尽量在上方的黑边显示弹幕,当屏幕内没有黑边时,需要避免弹幕显示过多导致严重影响观影体验。
实现方案
1. 横向和纵向断点系统
当前断点系统只有横向断点320vp、600vp、840vp、1440vp四个阈值,只用横向断点无法区分直板机、大小折叠机、Pad、HiCar等各种屏幕尺寸,需要增加纵向断点能力。通过横向和纵向断点实现页面布局和各种设备形态解耦,解决多设备布局类问题。
横向断点枚举值:
窗口宽度 | 横向断点 |
---|---|
<320vp | xs |
320-600vp | sm |
600-840vp | md |
840-1440vp | lg |
>1440vp | xl |
纵向断点根据屏幕Height/Width 高宽比划分两个阈值:
高宽比 | 纵向断点 |
---|---|
<0.8 | sm |
0.8-1.2 | md |
>1.2 | lg |
该断点系统使用举例:我们希望方屏设备都能支持视频全屏不旋转特性,则直接通过纵向断点为md作为判断条件,而大折叠也包含在该断点范围内,未来有新形态近似方屏设备都可以支持该特性,从而实现布局类特性和设备形态解耦。
参考代码
// 根据窗口宽度更新横向断点
updateWidthBreakpoint(): void {
let promise = window.getLastWindow(getContext(this));
promise.then((mainWindow: window.Window) => {
let windowRect: window.Rect = mainWindow.getWindowProperties().windowRect;
let windowWidthVp: number = windowRect.width / display.getDefaultDisplaySync().densityPixels;
let widthBp: string = '';
if (windowWidthVp < 320) {
widthBp = 'xs';
} else if (windowWidthVp >= 320 && windowWidthVp < 600) {
widthBp = 'sm';
} else if (windowWidthVp >= 600 && windowWidthVp < 840) {
widthBp = 'md';
} else if (windowWidthVp >= 840 && windowWidthVp < 1440) {
widthBp = 'lg';
} else {
widthBp = 'xl';
}
AppStorage.setOrCreate('widthBreakpoint', widthBp);
}).catch((err: BusinessError) => {
console.error(`Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`);
});
}
// 根据窗口宽高比更新纵向断点
updateHeightBreakpoint(): void {
let promise = window.getLastWindow(getContext(this));
promise.then((mainWindow: window.Window) => {
let windowRect: window.Rect = mainWindow.getWindowProperties().windowRect;
let windowWidthVp: number = windowRect.width / display.getDefaultDisplaySync().densityPixels;
let windowHeightVp: number = windowRect.height / display.getDefaultDisplaySync().densityPixels;
let heightBp: string = '';
let aspectRatio: number = windowHeightVp / windowWidthVp;
if (aspectRatio < 0.8) {
heightBp = 'sm';
} else if (aspectRatio >= 0.8 && aspectRatio < 1.2) {
heightBp = 'md';
} else {
heightBp = 'lg';
}
AppStorage.setOrCreate('heightBreakpoint', heightBp);
}).catch((err: BusinessError) => {
console.error(`Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`);
});
}
2. 视频全屏播放不旋转特性适配
根据上述断点系统方案,大折叠展开态落入纵向md的断点范围内。
以纵向断点为md作为判断条件,在屏幕尺寸为高宽比接近1:1的方屏时,调用window. setPreferredOrientation () 设置主窗口的显示方向属性为横竖屏旋转,从而实现视频详情页进入全屏播放页时全屏不旋转。
参考代码
let heightBreakpoint: string = AppStorage.get('heightBreakpoint') as string;
if (heightBreakpoint === 'md') {
let promise = window.getLastWindow(getContext(this));
promise.then((mainWindow: window.Window) => {
mainWindow.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
}).catch((err: BusinessError) => {
console.error(`Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`);
});
}
3.轻交互
3.1浅层窗口
简单的新建、筛选、添加、浏览,或临时的支付、登录、设置等页面,可以通过浅层窗口避免在宽屏设备上大幅度的页面跳转带来的体验中断。应用可根据自身业务诉求考虑需要调用半模态控件的场景,并选择适合的半模态控件,从而达到浅层窗口的体验。可根据业务特性,采用以下三种浅层窗口样式中的一种。
实现方案
手机端使用 半模态转场 实现。通过bindSheet属性为组件绑定半模态页面,在组件插入时可通过设置自定义或默认的内置高度确定半模态大小。
bindSheet(isShow: boolean, builder: CustomBuilder, options?: SheetOptions)
大折叠与PC/2in1端使用 自定义弹窗 实现。
CustomDialogController(value: CustomDialogControllerOptions)
3.2侧边面板
除浅层窗口外,在购物、浏览图片、浏览短视频、查看长视频等场景,可通过侧边面板实现边看边评、边看边买等便捷任务处理的轻交互体验。
实现方案
侧边面板可以使用 Row 实现。点击评论时控制子元素的宽度来实现侧边面板。
3.3长按预览
系统提供了长按预览的控件能力,接入控件后可以针对视频、附件等卡片实现长按预览播放、长按预览查看详情的体验,且可以在菜单中加入常用功能。
实现方案
长按手势事件属于交互归一 的一种基础输入方式。长按预览使用 LongPressGesture API实现。LongPressGesture可用于触发长按手势事件。
LongPressGesture(value?: { fingers?: number, repeat?: boolean, duration?: number })
4.跟手交互
4.1跟手弹框
大折叠展开态和平板的屏幕尺寸较大,弹出框上的操作按钮不易触达。建议针对大折叠展开态、平板等大尺寸的设备,提供跟手的弹出框。
跟手弹出框需要满足以下条件:
- 点击顶部,或底部的标题栏/工具栏上的图标/按钮等触发的弹出框,则使用该跟手弹出框样式。
- 跟手的弹框默认和触发按钮左右居中对齐,无法居中对齐时靠近边缘对齐。
实现方案
跟手弹框参考 气泡提示(Popup) 组件。Popup属性可绑定在组件上显示气泡弹窗提示,使用 bindPopup API给组件绑定popup弹窗。
bindPopup(show: boolean, popup: PopupOptions | CustomPopupOptions)
参考代码
@Entry
@Component
export struct PopupExample {
@State customPopup1: boolean = false
@State customPopup2: boolean = false
build() {
Row() {
Button("popup1")
.onClick(()=>{
this.customPopup1 = !this.customPopup1
})
// 给组件绑定Popup弹窗,靠近边缘对齐
.bindPopup(this.customPopup1, {
message: "this is a popup1",
popupColor: Color.Pink,
})
Blank()
Button("popup2")
.onClick(()=>{
this.customPopup2 = !this.customPopup2
})
// 给组件绑定Popup弹窗,靠近边缘对齐
.bindPopup(this.customPopup2, {
message: "this is a popup2",
popupColor: Color.Pink,
})
}
.width('100%')
.height('100%')
}
}