官方定义
定义:一套代码工程,一次开发上架,多端按需部署。
目标:支撑开发者快速高效的开发支持多种终端设备形态的应用,实现对不同设备兼容的同时,提供跨设备的流转、迁移和协同的分布式体验。
什么是1+8+N:1个系统HarmonyOS+8个常用终端(手机、平板、折叠屏、2in1、车机、手表等)+N个不同的物联网设备(智能家居等等)
为什么要做一多开发
随着终端设备形态日益多样化,分布式技术逐渐打破单一硬件边界,一个应用或服务,可以在不同的硬件设备之间随意调用、互助共享,让用户享受无缝的全场景体验。而作为应用开发者,广泛的设备类型也能为应用带来广大的潜在用户群体。但是如果一个应用需要在多个设备上提供同样的内容,则需要适配不同的屏幕尺寸和硬件,开发成本较高。HarmonyOS系统面向多终端提供了“一次开发,多端部署”(后文中简称为“一多”)的能力,让开发者可以基于一种设计,高效构建多端可运行的应用。
一多的主要问题
一多开发虽然听上去那么便利,但也一定会带来一些技术问题,主要是三个问题:
为了解决这三个问题,分别从不同的方式进行解决
- 页面适配问题:界面级一多
- 功能兼容问题:功能级一多
- 工程如何组织:工程级一多
界面级一多
界面级一多主要解决的问题是:
-
不同设备间的屏幕尺寸、色彩风格等存在差异,页面如何适配。
可以通过两中方式解决:
- 自适应布局
- 响应式布局
自适应布局是通过一些组件的自适应能力,来解决在小幅度的屏幕大小改变。如果屏幕变化跨度特别大,还是需要使用响应式布局。
自适应布局共有七种
针对常见的开发场景,方舟开发框架提炼了七种自适应布局能力,这些布局可以独立使用,也可多种布局叠加使用。
自适应布局类别 | 自适应布局能力 | 使用场景 | 实现方式 |
---|---|---|---|
自适应拉伸 | 拉伸能力 | 容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。 | Flex布局的flexGrow和flexShrink属性 |
均分能力 | 容器组件尺寸发生变化时,增加或减小的空间均匀分配给容器组件内所有空白区域。 | Row组件、Column组件或Flex组件的justifyContent属性设置为FlexAlign.SpaceEvenly | |
自适应缩放 | 占比能力 | 子组件的宽或高按照预设的比例,随容器组件发生变化。 | 基于通用属性的两种实现方式: - 将子组件的宽高设置为父组件宽高的百分比 - layoutWeight属性 |
缩放能力 | 子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。 | 布局约束的aspectRatio属性 | |
自适应延伸 | 延伸能力 | 容器组件内的子组件,按照其在列表中的先后顺序,随容器组件尺寸变化显示或隐藏。 | 基于容器组件的两种实现方式: - 通过List组件实现 - 通过Scroll组件配合Row组件或Column组件实现 |
隐藏能力 | 容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏。相同显示优先级的子组件同时显示或隐藏。 | 布局约束的displayPriority属性 | |
自适应折行 | 折行能力 | 容器组件尺寸发生变化时,如果布局方向尺寸不足以显示完整内容,自动换行。 | Flex组件的wrap属性设置为FlexWrap.Wrap |
响应式布局
自适应布局可以保证窗口尺寸在【一定范围内变化】时,页面的显示是正常的。但是将窗口尺寸【变化较大】时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题,此时就需要借助响应式布局能力调整页面结构。
响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。
响应式布局中最常使用的特征是窗口宽度,可以将窗口宽度划分为不同的范围(下文中称为断点)。当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。
三种响应式布局能力:
响应式布局能力 | 简介 |
断点 | 将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。 |
媒体查询 | 媒体查询支持监听窗口宽度、横竖屏、深浅色、设备类型等多种媒体特征,当媒体特征发生改变时同步调整页面布局。 |
栅格布局 | 栅格组件将其所在的区域划分为有规律的多列,通过调整不同断点下的栅格组件的参数以及其子组件占据的列数等,实现不同的布局效果。 |
断点
不同的断点取值范围
断点名称 | 取值范围(vp) | 设备 |
xs | [0, 320) | 手表等超小屏 |
sm | [320, 600) | 手机竖屏 |
md | [600, 840) | 手机横屏,折叠屏 |
lg | [840, +∞) | 平板,2in1 设备 |
系统提供了多种方法,判断应用当前处于何种断点,进而可以调整应用的布局。常见的监听断点变化的方法如下所示:
- 获取窗口对象并监听窗口尺寸变化(了解)
- 通过媒体查询监听应用窗口尺寸变化(掌握)
- 借助栅格组件能力监听不同断点的变化(掌握)
- 通过窗口对象,监听窗口尺寸变化(了解)
媒体查询
这里官网对通过媒体查询监听断点的功能做简单的封装,点击跳转。
这里做了更简化的版本
import { mediaquery } from '@kit.ArkUI'
interface Breakpoint {
name: string
size: number
mediaQueryListener?: mediaquery.MediaQueryListener
}
export const BreakpointKey: string = 'currentBreakpoint'
export class BreakpointSystem {
private currentBreakpoint: string = 'md'
private breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 }, { name: 'sm', size: 320 },
{ name: 'md', size: 600 }, { name: 'lg', size: 840 }
]
public register() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
let condition: string
if (index === this.breakpoints.length - 1) {
condition = '(' + breakpoint.size + 'vp<=width' + ')'
} else {
condition = '(' + breakpoint.size + 'vp<=width<' + this.breakpoints[index + 1].size + 'vp)'
}
console.log(condition)
breakpoint.mediaQueryListener = mediaquery.matchMediaSync(condition)
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(breakpoint.name)
}
})
})
}
public unregister() {
this.breakpoints.forEach((breakpoint: Breakpoint) => {
if (breakpoint.mediaQueryListener) {
breakpoint.mediaQueryListener.off('change')
}
})
}
private updateCurrentBreakpoint(breakpoint: string) {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint
AppStorage.set<string>(BreakpointKey, this.currentBreakpoint)
console.log('on current breakpoint: ' + this.currentBreakpoint)
}
}
}
/*
定义一个接口类型
键: 断点值
值: 泛型
*/
declare interface BreakPointTypeOption<T> {
xs?: T
sm?: T
md?: T
lg?: T
xl?: T
xxl?: T
}
/*
对外导出一个类
在实例化的时候接收一个泛型
*/
export class BreakPointType<T> {
// 选项对象
options: BreakPointTypeOption<T>
constructor(option: BreakPointTypeOption<T>) {
this.options = option
}
getValue(currentBreakPoint: string) {
if (currentBreakPoint === 'xs') {
return this.options.xs
} else if (currentBreakPoint === 'sm') {
return this.options.sm
} else if (currentBreakPoint === 'md') {
return this.options.md
} else if (currentBreakPoint === 'lg') {
return this.options.lg
} else if (currentBreakPoint === 'xl') {
return this.options.xl
} else if (currentBreakPoint === 'xxl') {
return this.options.xxl
} else {
return undefined
}
}
}
栅格布局
栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格可以显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。
- GridRow breakpoints属性 和 的 onBreakpointChange 事件
- GridRow 的 gutter属性、GridCol 的 offset 属性
- GridRow的 columns 属性、GridCol 的 span 属性
功能级一多
主要是应用如何解决设备系统能力差异的兼容问题。暂时用的比较少。
基本利用canIUse去查询设备是否具有某个能力如:拍照,定位,蓝牙等。
工程级一多
这部分对程序员来说非常重要,一个一多项目,要想更高效,更利于团队配合,就要好好思考,如果做项目结构分层。
一多模式下,官方推荐在开发过程中采用"三层工程架构",其实就是把项目拆分成不同类型的模块,再通过模块之间的引用组合,最终实现应用功能,拆分规范如下:
- commons(公共能力层):用于存放公共基础能力合集,比如工具库,公共配置等
- features(基础特性层):用于存放应用中相对独立的各个功能的UI以及业务逻辑实现
- products(产品定制层):用于针对不同设备形态进行功能和特性集成,作为应用入口
根据一个更具体的 手机-折叠屏-平板 三终端项目的话,应该如下设计
products:只是一个入口,应该不负责具体的业务,可以写业务
features:写具体业务,把每个业务拆分成具体的模块 N个,方便维护
commons: 提供基础组件-基础工具-基础类型- 架构组-技术平台组
那么不同的工程架构要用到什么包呢?
首先先了解都有哪几种包
包的分类
- hap- 可以有ability,可以有页面,可以有组件,可以有资源,不同平台的主包(手机包、平板包、手表包)
- hsp-动态共享包。运行时复用- 可以实现按需打包,最常用分包(用户模块包、购物车模块包、收获地址包,各种页面包)
- har- 静态共享包。编译态复用,常见于三方仓库包(请求库包、各种工具包)
还有一些注意点,har包是在引用的时候重复拷贝,如果项目中不是发布到三方库或二方库的话,可以使用hsp包,他在引用的时候赋值地址,不会重复拷贝。