鸿蒙(设备场景适配)——折叠屏应用指南(二)

往期推文全新看点

折叠屏悬停

折叠屏悬停描述折叠屏中的折痕避让和悬停适配,并介绍了折叠屏悬停的一些典型场景。

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("vedio zone").height("100%").width("100%").textAlign(TextAlign.Center).fontSize(25)
        }.backgroundColor(Color.Pink).width("100%").height("100%").id("upperitemsId")

        // 下列两个Column堆叠在下半屏区域
        Column() {
          Text("vedio title")
            .width("100%")
            .height(50)
            .textAlign(TextAlign.Center)
            .backgroundColor(Color.Red)
            .fontSize(25)
        }.width("100%").height("100%").justifyContent(FlexAlign.Start)

        Column() {
          Text("vedie 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.典型案例

悬停状态下,界面布局应自动调整。即将浏览型内容上移,在上半屏显示;将操作类控件元素下沉,在下半屏显示。提供更舒适和高效的使用体验。

视频

悬停时,上半屏显示视频画面,下半屏显示视频相关的操作按钮或周边信息。

悬停态沉浸式看长视频示例
图1 悬停态在视频详情页看长视频示例

视频通话

悬停时,上半屏显示通话界面,下半屏显示通话相关的操作按钮。

图2 悬停态的多方通话示例

图3 悬停态的双方通话示例

短视频

短视频悬停时,头像、评论、视频画面等显示的内容在上半屏展示,输入框等操作在下半屏显示。短视频类应用进行需要手势操作快速切换视频内容。建议下半屏支持横向滑动切换视频。

健身视频

悬停时,上半屏显示动作跟练视频内容、进度,下半屏显示播放及切换功能。

**图4 **悬停态看健身视频示例

拍摄

悬停时,上半屏显示取景画面,下半屏显示取景模式、拍摄参数控制按钮等操作类功能。

音频播放

悬停时,上半屏显示专辑封面或歌词、音乐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等各种屏幕尺寸,需要增加纵向断点能力。通过横向和纵向断点实现页面布局和各种设备形态解耦,解决多设备布局类问题。

横向断点枚举值:

窗口宽度横向断点
<320vpxs
320-600vpsm
600-840vpmd
840-1440vplg
>1440vpxl

纵向断点根据屏幕Height/Width ****高宽比划分两个阈值:

高宽比纵向断点
<0.8sm
0.8-1.2md
>1.2lg

该断点系统使用举例:我们希望方屏设备都能支持视频全屏不旋转特性,则直接通过纵向断点为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)

折叠屏与2in1端使用 自定义弹窗 实现。

CustomDialogController(value: CustomDialogControllerOptions)

3.2侧边面板

除浅层窗口外,在购物、浏览图片、浏览短视频、查看长视频等场景,可通过侧边面板实现边看边评、边看边买等便捷任务处理的轻交互体验。

实现方案

侧边面板可以使用 Row 组件嵌套两个 Column 实现。点击评论时控制子元素的宽度来实现侧边面板。

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%')
  }
}

最后

总是有很多小伙伴反馈说:鸿蒙开发不知道学习哪些技术?不知道需要重点掌握哪些鸿蒙开发知识点? 为了解决大家这些学习烦恼。在这准备了一份很实用的鸿蒙全栈开发学习路线与学习文档给大家用来跟着学习。

针对一些列因素,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植……等)技术知识点。

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://docs.qq.com/doc/DSEd0U29uT3hHbFZK

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):

在这里插入图片描述

《OpenHarmony源码解析》:

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://docs.qq.com/doc/DSHRVT0NZb05PUklN

图片

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值