【鸿蒙开发】第三十五章 一次开发多端部署
1 概述
定义:一套代码工程,一次开发上架,多端按需部署。
2 工程管理
2.1 工程创建
可以看到DevEco Studio创建出的默认工程,仅包含一个的entry类型的模块。
工程结构示例如下所示:
/application
├── common # 公共特性目录
│
├── features # 功能模块目录
│ ├── feature1 # 子功能
│ ├── feature2 # 子功能2
│ └── ... # 子功能n
│
└── product # 产品层目录
├── wearable # 智能穿戴泛类目录
├── default # 默认设备泛类目录
└── ...
2.2 新建Module
参考开发ohpm包,新建三个ohpm模块,分别命名为common、feature1、feature2。参考添加/删除Module,新建一个entry类型的模块,假设命名为“wearable”(仅仅为了说明某一类产品)。示例如下:
说明
在一个工程中同一个设备类型只支持一个Entry类型的模块。
2.3 修改Module配置
1、修改Module名称
修改创建工程时默认的entry模块名称。在该模块上点击鼠标右键,依次选择”Refactor -> Rename”,将名称修改为default。
2、修改Module类型及其设备类型
通过修改每个模块中的配置文件(module.json5)对模块进行配置,配置文件中各字段含义详见配置文件说明。
将default模块的deviceTypes配置为["phone", "tablet"],同时将其type字段配置为entry。 即default模块编译出的HAP在手机和平板上安装和运行。
-
将wearable模块的deviceTypes配置为["wearable"],同时将其type字段配置为entry。
即wearable模块编译出的HAP仅在智能穿戴设备上安装和运行。
2.4 调整目录结构
1、调整目录结构
1. 在工程根目录(MyApplication)上点击鼠标右键,依次选择“New -> Directory”新建子目录。创建product和features两个子目录。
2. 用鼠标左键将default目录拖拽到新建的product目录中,在DevEco Studio弹出的确认窗口中,点击“Refactor”即可。
3. 按照同样的步骤,将wearable目录放到product目录中,将feature1和feature2放到features目录中。
2.5 修改依赖关系
回顾之前小节中关于“工程结构”的介绍,我们推荐在common目录中存放基础公共代码,features目录中存放相对独立的功能模块代码,product目录中存放完全独立的产品代码。这样在product目录中依赖features和common中的公共代码来实现功能,可以最大程度实现代码复用。
配置依赖关系可以通过修改模块中的oh-package.json5文件。如下图所示,通过修改default模块中的oh-package.json5文件,使其可以使用common、feature1和feature2模块中的代码。更多详情参考配置系统ohpm包依赖。
同样的,修改feature1和feature2模块中的oh-package.json5文件,使其可以使用common模块中的代码。
修改oh-package.json5文件后,请点击右上角的“Sync Now”,否则改动不会生效。
2.6 引用ohpm包中的代码
在开发ohpm包中,仅介绍了如何使用ohpm包中的页面和资源,本小节以例子的形式补充介绍如何使用ohpm包中的类和函数。
示例如下:
在common模块中新增ComplexNumber类,用于表征复数(数学概念,由实部和虚部组成),该类包含toString()方法,将复数转换为字符形式。
在common模块中新增Add函数,用于计算并返回两个数字的和。
在default模块中,使用common模块新增的ComplexNumber类和Add函数。
-
在”common/src/main/ets”目录中,按照需要新增文件和自定义类和函数。
-
在”common/index.ets”文件中,申明需要export的类、函数的名称及在当前模块中的位置,否则其它模块无法使用。
-
在default模块中import和使用这些类和函数。注意提前在default模块的oh-package.json5文件中配置对common模块的依赖关系。
说明
如果需要将ohpm包发布供其他开发者使用,具体可参考发布ohpm包。
3 自适应布局
自适应布局可以保证窗口尺寸在一定范围内变化时,页面的显示是正常的。但是将窗口尺寸变化较大时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题,此时就需要借助响应式布局能力调整页面结构。
响应式布局是指页面内的元素可以根据特定的特征(如窗口宽度、屏幕方向等)自动变化以适应外部容器变化的布局能力。响应式布局中最常使用的特征是窗口宽度,可以将窗口宽度划分为不同的范围(下文中称为断点)。当窗口宽度从一个断点变化到另一个断点时,改变页面布局(如将页面内容从单列排布调整为双列排布甚至三列排布等)以获得更好的显示效果。
当前系统提供了如下三种响应式布局能力。
响应式布局能力 | 简介 |
---|---|
断点 | 将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。 |
媒体查询 | 媒体查询支持监听窗口宽度、横竖屏、深浅色、设备类型等多种媒体特征,当媒体特征发生改变时同步调整页面布局。 |
栅格布局 | 栅格组件将其所在的区域划分为有规律的多列,通过调整不同断点下的栅格组件的参数以及其子组件占据的列数等,实现不同的布局效果。 |
3.1 断点
断点以应用窗口宽度为切入点,将应用窗口在宽度维度上分成了几个不同的区间即不同的断点,在不同的区间下,开发者可根据需要实现不同的页面布局效果。具体的断点如下所示。
断点名称 | 取值范围(vp) |
---|---|
xs | [0, 320) |
sm | [320, 600) |
md | [600, 840) |
lg | [840, +∞) |
说明
以设备屏幕宽度作为参照物,也可以实现类似的效果。考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。
开发者可以根据实际使用场景决定适配哪些断点。如xs断点对应的一般是智能穿戴类设备,如果确定某页面不会在智能穿戴设备上显示,则可以不适配xs断点。
可以根据实际需要在lg断点后面新增xl、xxl等断点,但注意新增断点会同时增加UX设计师及应用开发者的工作量,除非必要否则不建议盲目新增断点。
系统提供了多种方法,判断应用当前处于何种断点,进而可以调整应用的布局。常见的监听断点变化的方法如下所示:
获取窗口对象并监听窗口尺寸变化
通过媒体查询监听应用窗口尺寸变化
借助栅格组件能力监听不同断点的变化
通过窗口对象监听断点变化的核心是获取窗口对象及注册窗口尺寸变化的回调函数。
1. 在UIAbility的onWindowStageCreate生命周期回调中,通过窗口对象获取启动时的应用窗口宽度并注册回调函数监听窗口尺寸变化。将窗口尺寸的长度单位由px换算为vp后,即可基于前文中介绍的规则得到当前断点值,此时可以使用状态变量记录当前的断点值方便后续使用。
// MainAbility.ts
import { window, display } from '@kit.ArkUI'
import { UIAbility } from '@kit.AbilityKit'
export default class MainAbility extends UIAbility {
private windowObj?: window.Window
private curBp: string = ''
//...
// 根据当前窗口尺寸更新断点
private updateBreakpoint(windowWidth: number) :void{
// 拿到当前窗口对象获取当前所在displayId
let displayId = this.windowObj?.getWindowProperties().displayId
try {
// 将长度的单位由px换算为vp
let windowWidthVp = windowWidth / display.getDisplayByIdSync(displayId).densityPixels
let newBp: string = ''
if (windowWidthVp < 320) {
newBp = 'xs'
} else if (windowWidthVp < 600) {
newBp = 'sm'
} else if (windowWidthVp < 840) {
newBp = 'md'
} else {
newBp = 'lg'
}
if (this.curBp !== newBp) {
this.curBp = newBp
// 使用状态变量记录当前断点值
AppStorage.setOrCreate('currentBreakpoint', this.curBp)
}
} catch(err) {
console.log("getDisplayByIdSync failed err"+err.code)
}
}
onWindowStageCreate(windowStage: window.WindowStage) :void{
windowStage.getMainWindow().then((windowObj) => {
this.windowObj = windowObj
// 获取应用启动时的窗口尺寸
this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
// 注册回调函数,监听窗口尺寸变化
windowObj.on('windowSizeChange', (windowSize)=>{
this.updateBreakpoint(windowSize.width)
})
});
// ...
}
//...
}
2. 在页面中,获取及使用当前的断点。
@Entry
@Component
struct Index {
@StorageProp('currentBreakpoint') curBp: string = 'sm'
build() {
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}) {
Text(this.curBp).fontSize(50).fontWeight(FontWeight.Medium)
}
.width('100%')
.height('100%')
}
}
3. 运行及验证效果。
| | |
3.2 媒体查询
在实际应用开发过程中,开发者常常需要针对不同类型设备或同一类型设备的不同状态来修改应用的样式。媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等,因此在应用开发过程中使用的非常广泛。
说明
类Web开发范式,支持在js文件和css文件中使用媒体查询,请查看js媒体查询和css媒体查询了解详细用法。
示例:
通过媒体查询,监听应用窗口宽度变化,获取当前应用所处的断点值。
| | |
1.对通过媒体查询监听断点的功能做简单的封装,方便后续使用
// common/breakpointsystem.ets
import { mediaquery } from '@kit.ArkUI'
export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
export interface Breakpoint {
name: BreakpointType
size: number
mediaQueryListener?: mediaquery.MediaQueryListener
}
export class BreakpointSystem {
private static instance: BreakpointSystem
private readonly breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 },
{ name: 'sm', size: 320 },
{ name: 'md', size: 600 },
{ name: 'lg', size: 840 }
]
private states: Set<BreakpointState<Object>>
private constructor() {
this.states = new Set()
}
public static getInstance(): BreakpointSystem {
if (!BreakpointSystem.instance) {
BreakpointSystem.instance = new BreakpointSystem();
}
return BreakpointSystem.instance
}
public attach(state: BreakpointState<Object>): void {
this.states.add(state)
}
public detach(state: BreakpointState<Object>): void {
this.states.delete(state)
}
public start() {
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)`
}
breakpoint.mediaQueryListener = mediaquery.matchMediaSync(condition)
if (breakpoint.mediaQueryListener.matches) {
this.updateAllState(breakpoint.name)
}
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateAllState(breakpoint.name)
}
})
})
}
private updateAllState(type: BreakpointType): void {
this.states.forEach(state => state.update(type))
}
public stop() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
if (breakpoint.mediaQueryListener) {
breakpoint.mediaQueryListener.off('change')
}
})
this.states.clear()
}
}
export interface BreakpointOptions<T> {
xs?: T
sm?: T
md?: T
lg?: T
xl?: T
xxl?: T
}
export class BreakpointState<T extends Object> {
public value: T | undefined = undefined;
private options: BreakpointOptions<T>
constructor(options: BreakpointOptions<T>) {
this.options = options
}
static of<T extends Object>(options: BreakpointOptions<T>): BreakpointState<T> {
return new BreakpointState(options)
}
public update(type: BreakpointType): void {
if (type === 'xs') {
this.value = this.options.xs
} else if (type === 'sm') {
this.value = this.options.sm
} else if (type === 'md') {
this.value = this.options.md
} else if (type === 'lg') {
this.value = this.options.lg
} else if (type === 'xl') {
this.value = this.options.xl
} else if (type === 'xxl') {
this.value = this.options.xxl
} else {
this.value = undefined
}
}
}
2. 在页面中,通过媒体查询,监听应用窗口宽度变化,获取当前应用所处的断点值
// MediaQuerySample.ets
import { BreakpointSystem, BreakpointState } from '../common/breakpointsystem'
@Entry
@Component
struct MediaQuerySample {
@State compStr: BreakpointState<string> = BreakpointState.of({ sm: "sm", md: "md", lg: "lg" })
@State compImg: BreakpointState<Resource> = BreakpointState.of({
sm: $r('app.media.sm'),
md: $r('app.media.md'),
lg: $r('app.media.lg')
});
aboutToAppear() {
BreakpointSystem.getInstance().attach(this.compStr)
BreakpointSystem.getInstance().attach(this.compImg)
BreakpointSystem.getInstance().start()
}
aboutToDisappear() {
BreakpointSystem.getInstance().detach(this.compStr)
BreakpointSystem.getInstance().detach(this.compImg)
BreakpointSystem.getInstance().stop()
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Column()
.height(100)
.width(100)
.backgroundImage(this.compImg.value)
.backgroundImagePosition(Alignment.Center)
.backgroundImageSize(ImageSize.Contain)
Text(this.compStr.value)
.fontSize(24)
.margin(10)
}
.width('100%')
.height('100%')
}
}
3.2 栅格布局
1、简介
栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格可以显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。
栅格的样式由Margin、Gutter、Columns三个属性决定。
Margin是相对应用窗口、父容器的左右边缘的距离,决定了内容可展示的整体宽度。
Gutter是相邻的两个Column之间的距离,决定内容间的紧密程度。
Columns是栅格中的列数,其数值决定了内容的布局复杂度。
单个Column的宽度是系统结合Margin、Gutter和Columns自动计算的,不需要也不允许开发者手动配置。
栅格布局就是栅格结合了断点,实现栅格布局能力的组件叫栅格组件。在实际使用场景中,可以根据需要配置不同断点下栅格组件中元素占据的列数,同时也可以调整Margin、Gutter、Columns的取值,从而实现不同的布局效果。
sm断点 | md断点 |
---|---|
| |
说明
ArkUI在API 9对栅格组件做了重构,推出了新的栅格组件GridRow和GridCol,同时原有的GridContainer组件及栅格设置已经废弃。
本文中提到的栅格组件,如无特别说明,都是指GridRow和GridCol组件。
2、栅格组件的断点
栅格组件提供了丰富的断点定制能力。
(一)开发者可以修改断点的取值范围,支持启用最多6个断点。
-
基于本文断点小节介绍的推荐值,栅格组件默认提供xs、sm、md、lg四个断点。
-
栅格组件支持开发者修改断点的取值范围,除了默认的四个断点,还支持开发者启用xl和xxl两个额外的断点。
说明
断点并非越多越好,通常每个断点都需要开发者“精心适配”以达到最佳显示效果。
示例1:
修改默认的断点范围,同时启用xl和xxl断点。
图片右下角显示了当前设备屏幕的尺寸(即应用窗口尺寸),可以看到随着窗口尺寸发生变化,栅格的断点也相应发生了改变(为了便于理解,下图中将设备的DPI设置为160,此时1vp=1px)。
@Entry
@Component
struct GridRowSample1 {
@State private currentBreakpoint: string = 'unknown'
build() {
// 修改断点的取值范围同时启用更多断点,注意,修改的断点值后面必须加上vp单位。
GridRow({breakpoints: {value: ['600vp', '700vp', '800vp', '900vp', '1000vp'],
reference: BreakpointsReference.WindowSize}}) {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12, xl: 12, xxl:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.currentBreakpoint).fontSize(50).fontWeight(FontWeight.Medium)
}
}
}.onBreakpointChange((currentBreakpoint: string) => {
this.currentBreakpoint = currentBreakpoint
})
}
}
(二)栅格断点默认以窗口宽度为参照物,同时还允许开发者配置为以栅格组件本身的宽度为参照物。
栅格既可以用于页面整体布局的场景,也可以用于页面局部布局的场景。考虑到在实际场景中,存在应用窗口尺寸不变但是局部区域尺寸发生了变化的情况,栅格组件支持以自身宽度为参照物响应断点变化具有更大的灵活性。
示例2:
以栅格组件宽度为参考物响应断点变化。满足窗口尺寸不变,而部分内容区需要做响应式变化的场景。
为了便于理解,下图中自定义预览器的设备屏幕宽度设置为650vp。示例代码中将侧边栏的变化范围控制在[100vp, 600vp],那么右侧的栅格组件宽度相对应在[550vp, 50vp]之间变化。根据代码中对栅格断点的配置,栅格组件宽度发生变化时,其断点相应的发生改变。
@Entry
@Component
struct GridRowSample2 {
@State private currentBreakpoint: string = 'unknown';
build() {
// 用户可以通过拖拽侧边栏组件中的分隔线,调整侧边栏和内容区的宽度。
SideBarContainer(SideBarContainerType.Embed)
{
// 侧边栏,尺寸变化范围 [100vp, 600vp]
Column(){}.width('100%').backgroundColor('#19000000')
// 内容区,尺寸变化范围 [550vp, 50vp]
GridRow({breakpoints: {value: ['100vp', '200vp', '300vp', '400vp', '500vp'],
reference: BreakpointsReference.ComponentSize}}) {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12, xl: 12, xxl:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.currentBreakpoint).fontSize(50).fontWeight(FontWeight.Medium)
}
}
}.onBreakpointChange((currentBreakpoint: string) => {
this.currentBreakpoint = currentBreakpoint;
}).width('100%')
}
// 侧边栏拖拽到最小宽度时,不自动隐藏
.autoHide(false)
.sideBarWidth(100)
// 侧边栏的最小宽度
.minSideBarWidth(100)
// 侧边栏的最大宽度
.maxSideBarWidth(600)
}
}
(三)栅格组件的断点发生变化时,会通过onBreakPointChange事件通知开发者。
在之前的两个例子中,已经演示了onBreakpointChange事件的用法,此处不再赘述。
3、栅格组件的columns、gutter和margin
栅格组件columns默认为12列,gutter默认为0,同时支持开发者根据实际需要定义不同断点下的columns数量以及gutter长度。特别的,在栅格组件实际使用过程中,常常会发生多个元素占据的列数相加超过总列数而折行的场景。栅格组件还允许开发者分别定义水平方向的gutter(相邻两列之间的间距)和垂直方向的gutter(折行时相邻两行之间的间距)。
考虑到组件通用属性中已经有margin和padding,栅格组件不再单独提供额外的margin属性,直接使用通用属性即可。借助margin或者padding属性,均可以控制栅格组件与父容器左右边缘的距离,但是二者也存在一些差异:
-
margin区域在栅格组件的边界外,padding区域在栅格组件的边界内。
-
栅格组件的backgroundColor会影响padding区域,但不会影响margin区域。
总的来讲,margin在组件外而padding在组件内,开发者可以根据实际需要进行选择及实现目标效果。
示例3:
不同断点下,定义不同的columns和gutter。
sm | md | lg |
---|---|---|
| |
@Entry
@Component
struct GridRowSample3 {
private bgColors: ResourceColor[] = [
$r('sys.color.ohos_id_color_palette_aux1'),
$r('sys.color.ohos_id_color_palette_aux2'),
$r('sys.color.ohos_id_color_palette_aux3'),
$r('sys.color.ohos_id_color_palette_aux4'),
$r('sys.color.ohos_id_color_palette_aux5'),
$r('sys.color.ohos_id_color_palette_aux6')
]
build() {
// 配置不同断点下columns和gutter的取值
GridRow({columns: {sm: 4, md: 8, lg: 12},
gutter: {x: {sm: 8, md: 16, lg: 24}, y: {sm: 8, md: 16, lg: 24}}}) {
ForEach(this.bgColors, (bgColor:ResourceColor)=>{
GridCol({span: {sm: 2, md: 2, lg: 2}}) {
Row().backgroundColor(bgColor).height(30).width('100%')
}
})
}
}
}
示例4:
通过通用属性margin或者padding,均可以控制栅格组件与其父容器左右两侧的距离,但padding区域计算在栅格组件内而margin区域计算在栅格组件外。此外,借助onBreakpointChange事件,还可以改变不同断点下margin或padding值。
@Entry
@Component
struct GridRowSample4 {
@State private gridMargin: number = 0
build() {
Column() {
Row().width('100%').height(30)
// 使用padding控制栅格左右间距
GridRow() {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text("padding").fontSize(24).fontWeight(FontWeight.Medium)
}.backgroundColor('#19000000').width('100%')
}
}
.height(50)
.borderWidth(2)
.borderColor('#F1CCB8')
.padding({left: this.gridMargin, right: this.gridMargin})
// 借助断点变化事件配置不同断点下栅格组件的左右间距值
.onBreakpointChange((currentBreakpoint: string) => {
if (currentBreakpoint === 'lg' || currentBreakpoint === 'md') {
this.gridMargin = 24
} else {
this.gridMargin = 12
}
})
Row().width('100%').height(30)
// 使用margin控制栅格左右间距
GridRow() {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text("margin").fontSize(24).fontWeight(FontWeight.Medium)
}.backgroundColor('#19000000').width('100%')
}
}
.height(50)
.borderWidth(2)
.borderColor('#F1CCB8')
.margin({left: this.gridMargin, right: this.gridMargin})
}
}
}
4、栅格组件的span、offset和order
栅格组件(GridRow)的直接孩子节点只可以是栅格子组件(GridCol),GridCol组件支持配置span、offset和order三个参数。这三个参数的取值按照"xs -> sm -> md -> lg -> xl -> xxl"的向后方向具有继承性(不支持向前方向的继承性),例如将sm断点下span的值配置为3,不配置md断点下span的值,则md断点下span的取值也是3。
参数名 | 类型 | 必填 | 默认值 | 说明 |
---|---|---|---|---|
span | {xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number} | 是 | - | 在栅格中占据的列数。span为0,意味着该元素既不参与布局计算,也不会被渲染。 |
offset | {xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number} | 否 | 0 | 相对于前一个栅格子组件偏移的列数。 |
order | {xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number} | 否 | 0 | 元素的序号,根据栅格子组件的序号,从小到大对栅格子组件做排序。 |
示例5:
通过span参数配置GridCol在不同断点下占据不同的列数。特别的,将md断点下3和6的span配置为0,这样在md断点下3和6不会渲染和显示。
sm | md | lg |
---|---|---|
| | |
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample5 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: (item.index % 3 === 0) ? 0 : 4, lg: 3}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例6:
通过offset参数,配置GridCol相对其前一个兄弟间隔的列数。
sm | md | lg |
---|---|---|
| | |
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample6 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}, offset: {sm: 0, md: 2, lg: 1} }) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例7:
通过order属性,控制GridCol的顺序。在sm和md断点下,按照1至6的顺序排列显示;在lg断点下,按照6至1的顺序排列显示。
sm | md | lg |
---|---|---|
| | |
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample7 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}, order: {lg: (6-item.index)}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例8:
仅配置sm和lg断点下span、offset和order参数的值,则md断点下这三个参数的取值与sm断点相同(按照“sm->md->lg”的向后方向继承)。
sm | md | lg |
---|---|---|
| | |
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample8 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
// 不配置md断点下三个参数的值,则其取值与sm断点相同
GridCol({span: {sm:4, lg: 3}, offset: {sm: 2, lg: 1},
order: {sm: (6-item.index), lg: item.index}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
5、栅格组件的嵌套使用
栅格组件可以嵌套使用以满足复杂场景的需要。
示例9:
sm | md | lg |
---|---|---|
| | |
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample9 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
GridCol({span: {sm: 12, md: 10, lg: 8}, offset: {sm: 0, md: 1, lg: 2}}) {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
.backgroundColor('#19000000')
.height('100%')
}
}
}
}
4 交互归一
对于不同类型的智能设备,用户可能有不同的交互方式,如通过触摸屏、鼠标、触控板等。如果针对不同的交互方式单独做适配,会增加开发工作量同时产生大量重复代码。为解决这一问题,我们统一了各种交互方式的API,即实现了交互归一。
4.1 基础输入
常见的基础输入方式及其在各输入设备上的表现如下图所示。
基础输入对应的开发接口,以及当前支持情况如下表所示。
输入 | 开发接口 | 触控屏 | 触控板 | 鼠标 |
---|---|---|---|---|
悬浮 | onHover | NA | √ | √ |
点击 | onClick | √ | √ | √ |
双击 | TapGesture | √ | √ | √ |
长按 | LongPressGesture | √ | × | √ |
上下文菜单 | ContentMenu | √ | √ | √ |
拖拽 | Drag | √ | √ | √ |
轻扫 | SwipeGesture | √ | √ | √ |
滚动及平移 | PanGesture | √ | √ | √ |
缩放 | PinchGesture | √ | √ | √ |
旋转 | RotationGesture | √ | √ | NA |
说明
点击事件(onClick)其实是点击手势(TapGesture)的一个特殊场景(单指单次点击)。该场景使用的非常广泛,为了方便开发者使用及符合传统开发习惯,所以专门提供了开发接口。
触控板支持长按输入的功能正在开发中。
4.2 拖拽事件
拖拽是应用开发中经常碰到的场景。拖拽发生在两个组件之间,它不是简单的单次输入,而是一个”过程”,通常包含如下步骤(以将组件A拖拽到组件B中为例)。
-
长按或点击组件A,触发拖拽。
-
保持按压或点击,持续将组件A向组件B拖拽。
-
抵达组件B中,释放按压点击,完成拖拽。
-
也可以在未抵达组件B的中途,释放按压点击,取消拖拽。
一个完整的拖拽事件,包含多个拖拽子事件,如下表所示(请访问拖拽事件了解详细用法)。当前触控屏和鼠标的拖拽事件已经实现”交互归一”,对手写笔的支持正在开发中。
名称 | 功能描述 |
---|---|
onDragStart | 绑定A组件,触控屏长按/鼠标左键按下后移动触发 |
onDragEnter | 绑定B组件,触控屏手指、鼠标移动进入B组件瞬间触发 |
onDragMove | 绑定B组件,触控屏手指、鼠标在B组件内移动触发 |
onDragLeave | 绑定B组件,触控屏手指、鼠标移动退出B组件瞬间触发 |
onDrop | 绑定B组件,在B组件内,触控屏手指抬起、鼠标左键松开时触发 |
在页面开发过程中,经常需要用到颜色、字体、间距、图片等资源,在不同的设备或配置中,这些资源的值可能不同。有两种方式处理:
-
应用资源:借助资源文件能力,开发者在应用中自定义资源,自行管理这些资源在不同的设备或配置中的表现。
-
系统资源:开发者直接使用系统预置的资源定义(即分层参数)。
4.3 应用资源
1、资源文件介绍
应用开发中使用的各类自定义资源文件,需要统一存放于应用的resources目录下,便于使用和维护。resources目录包括两大类目录,一类为base目录与限定词目录,另一类为rawfile目录,其基础目录结构如下所示。
resources
|---base // 默认存在的目录
| |---element
| | |---string.json
| |---media
| | |---icon.png
|---en_GB-vertical-car-mdpi // 限定词目录示例,需要开发者自行创建
| |---element
| | |---string.json
| |---media
| | |---icon.png
|---rawfile // rawfile目录
base目录默认存在,而限定词目录需要开发者自行创建,其名称可以由一个或多个表征应用场景或设备特征的限定词组合而成。应用使用某资源时,系统会根据当前设备状态优先从相匹配的限定词目录中寻找该资源。只有当resources目录中没有与设备状态匹配的限定词目录,或者在限定词目录中找不到该资源时,才会去base目录中查找。rawfile是原始文件目录,它不会根据设备状态去匹配不同的资源
说明
请访问声明式开发范式资源文件分类,了解限定词目录的命名规则、创建流程、匹配规则等,本文不展开介绍。
没有设备状态匹配的限定词目录,或者在限定词目录中找不到目标资源时,会继续在base目录中查找。强烈建议对于所有应用自定义资源都在base目录中定义默认值,防止出现找不到资源值的异常场景。
类Web开发范式的资源文件路径及资源限定词的使用与声明式范式不同,详情请参考类Web开发范式资源限定与访问及类Web开发范式文件组织。
base目录与限定词目录下面可以创建资源组目录(包括element、media等),用于存放特定类型的资源文件。
资源组目录 | 目录说明 | 资源文件 |
---|---|---|
element | 表示元素资源,以下每一类数据都采用相应的JSON文件来表征。 - boolean,布尔型 - color,颜色 - float,浮点型 - intarray,整型数组 - integer,整型 - pattern,样式 - plural,复数形式 - strarray,字符串数组 - string,字符串 | element目录中的文件名称建议与下面的文件名保持一致。每个文件中只能包含同一类型的数据。 - boolean.json - color.json - float.json - intarray.json - integer.json - pattern.json - plural.json - strarray.json - string.json |
media | 表示媒体资源,包括图片、音频、视频等非文本格式的文件。 | 文件名可自定义,例如:icon.png。 |
在element目录的各个资源文件中,以“name-value”的形式定义资源,如下所示。而在media目录中,直接以文件名作为name,故开发者将文件放入media目录即可,无需再额外定义name。
// color.json
{
"color": [
{
"name": "color_red",
"value": "#ffff0000"
},
{
"name": "color_blue",
"value": "#ff0000ff"
}
]
}
2、访问应用资源
在工程中,通过 "$r('app.type.name')" 的形式引用应用资源。app代表是应用内resources目录中定义的资源;type 代表资源类型(或资源的存放位置),可以取 color、float、string、plural和media,name代表资源命名,由开发者添加资源时确定。
3、示例
在应用的resources目录下,创建名为tablet的限定词子目录,并按照下表所示,在base目录和tablet限定词目录中添加相应的资源。
资源名称 | 资源类型 | base目录中资源值 | 限定词目录(tablet)中资源值 |
---|---|---|---|
my_string | string | default | tablet |
my_color | color | #ff0000 | #0000ff |
my_float | float | 60vp | 80vp |
my_image | media | my_image.png(太阳图标) | my_image.png(月亮图标) |
在代码中通过 "$r('app.type.name')" 的形式使用应用资源,并分别在默认设备和平板上查看代码的运行效果,可以发现同一资源在不同设备上的取值不同。
@Entry
@Component
struct Index {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text($r("app.string.my_string"))
.fontSize($r("app.float.my_float"))
.fontColor($r("app.color.my_color"))
Image($r("app.media.my_image"))
.width(100)
.height(100)
}
.width('100%')
.height('100%')
}
}
4.4 系统资源
除了自定义资源,开发者也可以使用系统中预定义的资源(即分层参数,同一资源ID在设备类型、深浅色等不同配置下有不同的取值)。
在开发过程中,分层参数的用法与资源限定词基本一致。开发者可以通过"$r('sys.type.resource_id')"的形式引用系统资源。sys代表是系统资源;type代表资源类型,值可以取color、float、string和media;resource_id代表资源id。
说明
仅声明式开发范式支持使用分层参数,类Web开发范式不支持。
系统资源可以保证不同团队开发出的应用有较为一致的视觉风格。对于系统预置应用,强烈建议使用系统资源;对于三方应用,可以根据需要选择使用系统资源或自定义应用资源。