鸿蒙5.0 设备场景实践——Pura X外屏开发实践

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


概述

3月20日,华为正式发布了“阔折叠”设计的手机——Pura X,该机型采用上下折叠的方式,配有一块16:10比例的内屏和一块1:1比例的方形外屏,兼顾大屏沉浸体验与单手便捷操作。阔折叠屏Pura X的外屏,在日常使用中大幅提升了效率和交互体验,用户无需展开手机即可快速完成高频操作,例如:

  • 即时信息处理:查看通知、日程、天气等关键信息。
  • 高效影音控制:切换歌曲、调节音量、预览视频进度等。
  • 便捷出行导航:步行或驾车时快速查看路线关键指引。
  • 快速移动支付:显示付款码,快捷支付。
  • 个性化表达:通过动态壁纸、定制图案等展现独特风格。

为充分发挥1:1方形外屏的优势,应用需针对其特殊比例进行适配,优化页面布局与交互逻辑,确保用户在外屏上也能获得流畅、直观的操作体验。

说明
Pura X开合连续规则:

  • 外屏切换到内屏,界面可以直接接续。
  • 内屏(锁屏或非锁屏状态)切换到外屏,默认都显示为锁屏的亮屏状态。用户解锁后:对于应用已适配外屏的情况下,应用界面可以接续到外屏。

在Pura X的外屏,应用窗口的高度默认减小至约内屏高度的一半。下文将从布局设计的维度,针对Pura X外屏常见的五种开发场景,给出推荐的设计方案与开发指导。

布局设计

本章节将介绍Pura X外屏上推荐的设计方案,保证页面布局能够完整显示,避免出现截断、挤压、堆叠等现象,并充分利用屏幕空间,提供最佳的用户体验。

独特的小窗口布局

说明
应用通常针对类方屏的小窗口页面会设计不同的布局,因此需要在代码中实现响应式布局。一多场景中所有的响应式布局都基于断点来开发,Pura X外屏场景的区分需要在项目中添加横纵断点。

实现原理

  1. Pura X的外屏,横向断点为sm,纵向断点为md,推荐使用独特的小窗口布局。
    以设置图片的高度为例,在Pura X外屏布局中高度为24vp,内屏时高度为48vp。使用横纵向断点判断,设置具体的属性值。
Image($r('app.media.icon_arrow_right'))
  .height(this.currentWidthBreakpoint === 'sm' && this.currentHeightBreakpoint === 'md' ? 24 : 48)
  .aspectRatio(1)
  1. 应用中有Pura X外屏布局中显示,内屏布局中隐藏的内容,使用visibility或if…else…结合横纵断点判断是否显示。
// 方案一
Column() {
  // 小窗口布局显示的内容
}
.visibility(this.currentWidthBreakpoint === 'sm' && this.currentHeightBreakpoint === 'md' ? Visibility.Visible :
  Visibility.None)

// 方案二
if (this.currentWidthBreakpoint === 'sm' && this.currentHeightBreakpoint === 'md') {
  Column() {
    // 小窗口布局显示的内容
  }
}

页面支持滑动

Pura X的外屏,窗口高度会减小至约内屏的1/2,可能导致内屏能够完整显示的内容在外屏上显示不全。

实现原理

设置Scroll组件的scrollBar属性为BarState.Off,控制滚动条不显示。当窗口高度足够显示页面全部内容时,Scroll组件自动失效,页面不可滑动;当窗口高度不足以显示页面全部内容时,Scroll组件自动生效,页面可以滑动。

Scroll() {
  Column() {
    // ...
  }
  .width('100%')
}
.scrollBar(BarState.Off)
.height('100%')
.width('100%')

短视频播放页面

Pura X外屏展示短视频播放页面,要求背景图片(视频)进行等比例缩放,并进行上下沉浸,上方沉浸至顶部标题栏,下方沉浸至底部页签栏。侧边控件可滑动,完整显示页面内容。

实现原理

使用Stack组件控制页面内容显示层级,控制背景图片上下沉浸,且互相不影响交互事件。Z层级由下到上分别是背景图片(视频)区、底部页签区、短视频描述区、侧边控件区、顶部页签区。顶部和底部页签设置内边距padding为topAvoidHeight或bottomAvoidHeight避让系统规避区。侧边控件区使用Scroll组件自动控制滑动是否生效,使用 Blank组件 和 displayPriority属性 控制侧边控件区上下两侧的留白,容器高度足够时上下留白,容器高度不足时自动隐藏。

Stack({ alignContent: Alignment.BottomEnd }) {
  // Background image.
  Row() {
    Image($r('app.media.background_image'))
      .height('100%')
      .objectFit(ImageFit.Cover)
      .aspectRatio(0.6)
  }
  .height('100%')
  .width('100%')
  .justifyContent(FlexAlign.Center)

  // Bottom tabs.
  List() {
    // ...
  }
  .backgroundColor('#99000000')
  .listDirection(Axis.Horizontal)
  .height(this.bottomBarHeight)
  .padding({ bottom: this.bottomAvoidHeight })
  // ...

  // Video description.
  Column() {
    // ...
  }
  .alignItems(HorizontalAlign.Start)
  .padding({
    left: $r('app.float.margin_md'),
    right: $r('app.float.margin_md')
  })
  // ...

  // Sidebar buttons.
  Scroll() {
    Column() {
      Blank()
        .layoutWeight(3)
        .displayPriority(1)
      // ...
      Blank()
        .layoutWeight(1)
        .displayPriority(1)
    }
    // ...
  }
  .scrollBar(BarState.Off)
  .layoutWeight(1)
  .width('56vp')
  .edgeEffect(EdgeEffect.None)
  .align(Alignment.Bottom)
  .margin({
    top: this.topAvoidHeight + 24,
    bottom: this.bottomBarHeight,
    right: '8vp'
  })

  // Top tabs.
  Row() {
    // ...
}
.height('100%')
.width('100%')
.backgroundColor(Color.Black)

自定义弹窗适配小窗口

在Pura X外屏上,当窗口高度无法完整显示自定义弹窗时,可能出现弹窗内容截断,需要进行自定义弹窗适配小窗口。效果图如下:

实现原理

使用constraintSize设置约束尺寸,自定义弹窗的最大高度不超过父组件高度的90%。同时最外层使用Scroll组件自动支持滚动。

Scroll() {
  Column() {
    // ...
  }
}
.scrollBar(BarState.Off)
.constraintSize({
  minHeight: 0,
  maxHeight: '90%'
})

滑动沉浸式浏览

在Pura X外屏的通用场景下,推荐上滑隐藏、下滑恢复显示。用户可以通过手指向上滑动屏幕临时隐藏掉标题栏、页签栏等界面元素,达到全屏浏览内容的效果,同时手指向下滑动屏幕时,标题栏和页签栏通过动画逐渐显示,从而可以更专注于应用展示的内容。效果图如下:

实现原理

通过滚动时动态调整页面组件高度和透明度,达到视觉上逐渐显示和隐藏的效果。

开发步骤

  1. 使用状态变量控制顶部标题栏、底部页签栏的高度和透明度。标题栏高度为topBarHeight,页签栏高度为bottomBarHeight,标题栏和页签栏的透明度为barOpacity。
@StorageLink('topBarHeight') topBarHeight: number = CommonConstants.UTIL_HEIGHTS[1] + this.topAvoidHeight;
@State bottomBarHeight: number = CommonConstants.UTIL_HEIGHTS[0] + this.bottomAvoidHeight;
@State barOpacity: number = 1;
  1. 在沉浸式布局下,标题栏高度在应用上下分屏时由固定值78vp+顶部系统规避区的高度topAvoidHeight组成;页签栏高度由固定值56vp+底部系统规避区的高度bottomAvoidHeight组成,页签栏的底部内边距padding为bottomAvoidHeight,避让底部系统导航条。
@StorageLink('topAvoidHeight') @Watch('topBarHeightChange') topAvoidHeight: number = 0;
@StorageLink('bottomAvoidHeight') @Watch('bottomBarHeightChange') bottomAvoidHeight: number = 0;
// ...
topBarHeightChange(): void {
  if (this.currentWidthBreakpoint === 'sm' && (this.currentHeightBreakpoint === 'md' ||
    this.currentHeightBreakpoint === 'sm')) {
    this.topBarHeight = 78 + this.topAvoidHeight;
  }
  // ...
};

bottomBarHeightChange(): void {
  this.bottomBarHeight = 56 + this.bottomAvoidHeight;
};
  1. 顶部和底部系统规避区高度会随应用窗口变化而变化,需要在窗口生命周期创建时调用window.getAvoidArea()获取初始的系统避让区高度,并使用window.on(‘avoidAreaChange’)监听系统避让区的变化,常见触发系统避让区回调的场景可参考 on(‘avoidAreaChange’) 。
export default class EntryAbility extends UIAbility {
  private windowUtil?: WindowUtil = WindowUtil.getInstance();
  private onAvoidAreaChange: (avoidArea: window.AvoidAreaOptions) => void = (avoidArea: window.AvoidAreaOptions) => {
    if (avoidArea.type === window.AvoidAreaType.TYPE_SYSTEM) {
      AppStorage.setOrCreate('topAvoidHeight', px2vp(avoidArea.area.topRect.height));
    } else if (avoidArea.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
      AppStorage.setOrCreate('bottomAvoidHeight', px2vp(avoidArea.area.bottomRect.height));
    }
  };
  // ...
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    this.windowUtil?.setWindowStage(windowStage);

    windowStage.getMainWindow((err: BusinessError, data: window.Window) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to get the main window. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      this.windowUtil!.setFullScreen();
      // ...
      let topAvoidHeight: window.AvoidArea = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      AppStorage.setOrCreate('topAvoidHeight', px2vp(topAvoidHeight.topRect.height));
      let bottomAvoidHeight: window.AvoidArea = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      AppStorage.setOrCreate('bottomAvoidHeight', px2vp(bottomAvoidHeight.bottomRect.height));
      data.on('avoidAreaChange', this.onAvoidAreaChange);
      if (AppStorage.get('currentWidthBreakpoint') === 'sm' && (AppStorage.get('currentHeightBreakpoint') === 'md' ||
        AppStorage.get('currentHeightBreakpoint') === 'sm')) {
        // Set top bar height when the application is in small screen.
        AppStorage.setOrCreate('topBarHeight', CommonConstants.UTIL_HEIGHTS[1] + px2vp(topAvoidHeight.topRect.height));
      } else {
        // Set top bar height when the application is in full screen.
        AppStorage.setOrCreate('topBarHeight', CommonConstants.UTIL_HEIGHTS[2] + px2vp(topAvoidHeight.topRect.height));
      }
    })
    // ...
  }
  // ...
}
  1. 设置顶部标题栏的高度为topBarHeight,透明度为barOpacity;设置底部页签栏的高度为bottomBarHeight,透明度为barOpacity,确保滑动时标题栏和页签栏能够逐渐显隐。列表内容在Stack组件内顶部外边距设置为topBarHeight,确保滑动沉浸式浏览时列表占满剩余高度。
Tabs() {
  TabContent() {
    Stack({ alignContent: Alignment.Top }) {
      Row() {
        Text($r('app.string.app_title'))
          .fontSize($r('app.float.font_size_xl'))
          .fontWeight(CommonConstants.FONT_WEIGHTS[1])
          .height(this.topBarHeight)
          .align(Alignment.Bottom)
          .padding({ bottom: 12 })
      }
      .height(this.topBarHeight)
      .opacity(this.barOpacity)
      // ...

      List({
        space: CommonConstants.LIST_SPACE[0],
        scroller: this.listScroller
      }) {
        // ...
      }
      .margin({ top: this.topBarHeight })
      // ...
    }
    .height('100%')
    .width('100%')
  }
  .tabBar(this.bottomTabBuilder(0))
  // ...
}
// ...
.barHeight(this.bottomBarHeight)
// ...
  1. 当横向断点为sm,纵向断点为sm或md,应用窗口属于Pura X外屏或手机上下分屏等小窗口时,在滑动过程中,如果当前Y轴滑动的偏移量>0上滑时且固定区(顶部标题栏和底部页签栏)未完全隐藏,逐渐减少固定区的高度和透明度,实现滑动过程隐藏的效果;如果当前Y轴滑动的偏移量<0下滑时且未显示恢复动画且固定区处于隐藏状态,则通过动画逐渐恢复固定区的高度和透明度。
.onScrollFrameBegin((offset: number) => {
  if (this.currentWidthBreakpoint !== 'sm' || (this.currentHeightBreakpoint !== 'md' &&
    this.currentHeightBreakpoint !== 'sm')) {
    return { offsetRemain: offset };
  }
  if (offset > 0 && !this.hideDone) {
    this.currentYOffset += offset;
    if (this.currentYOffset <= 100) {
      this.bottomBarHeight = this.bottomBarHeight * (1 - this.currentYOffset / 100);
      this.topBarHeight = this.topBarHeight * (1 - this.currentYOffset / 100);
      this.barOpacity = 1 - this.currentYOffset / 100;
    } else {
      this.topBarHeight = 0;
      this.bottomBarHeight = 0;
      this.barOpacity = 0;
      this.hideDone = true;
    }
    this.isHiding = true;
  }
  if (offset < 0 && this.isHiding) {
    this.hideDone = false;
    this.getUIContext().animateTo({
      duration: 300
    }, () => {
      this.bottomBarHeight = 56 + this.bottomAvoidHeight;
      this.topBarHeight = 78 + this.topAvoidHeight;
      this.barOpacity = 1;
      this.currentYOffset = 0;
      this.isHiding = false;
    });
  }
  return { offsetRemain: offset };
})
### 关于 Pura Harmony-Dialog 的使用方法 Pura `harmony-dialog` 是基于 HarmonyOS 平台的一个流行第三方组件,属于 `harmony-utils` 工具库的一部分。该工具库旨在帮助开发者快速实现对话框交互功能,提升用户体验的同时减少开发成本[^1]。 以下是关于如何使用 `pura harmony-dialog` 的基本介绍以及示例代码: #### 基本概念 `harmony-dialog` 提供了一种简单的方式来创建自定义对话框。它支持多种配置选项,例如标题、内容、按钮样式等。通过这些配置项,可以轻松定制适合不同场景的对话框界面。 #### 安装与引入 要使用此组件,需先将其集成到项目中。通常可以通过 npm 或其他包管理器安装依赖: ```bash npm install harmony-utils ``` 接着,在应用程序文件里导入所需的模块: ```javascript import { Dialog } from 'harmony-utils'; ``` #### 示例代码 下面展示了一个简单的例子来说明如何利用 `harmony-dialog` 创建并显示一个确认提示框: ```javascript // 初始化对话框参数 const dialogParams = { title: '警告', // 对话框标题 message: '您确定要删除这条记录吗?', // 主体消息 positiveText: '确定', // 正面操作文字 negativeText: '取消' // 负面操作文字 }; // 显示对话框 Dialog.confirm(dialogParams).then((result) => { if (result === true) { console.log('用户点击了“确定”'); // 执行相应逻辑... } else { console.log('用户点击了“取消”'); } }); ``` 上述脚本展示了如何调用静态方法 `.confirm()` 来弹出带有两个按钮的选择窗口,并处理用户的反馈结果。 #### 参考文档位置 对于更详细的 API 描述和高级特性,请查阅官方发布的鸿蒙学习手册中的 ArkTS 和 ArkUI 部分[^2]。这部分资料涵盖了从基础语法到复杂控件使用的全面指导,能够满足大多数实际需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值