HarmonyOS Developer 常见布局开发指导
响应式布局
栅格布局
栅格系统作为一种辅助布局的定位工具,在平面设计和网站设计都起到了很好的作用,对移动设备的界面设计有较好的借鉴作用。总结栅格系统对于移动设备的优势主要有:
- 给布局提供一种可循的规律,解决多尺寸多设备的动态布局问题。
- 给系统提供一种统一的定位标注,保证各模块各设备的布局一致性。
- 给应用提供一种灵活的间距调整方法,满足特殊场景布局调整的可能性。
推荐使用栅格组件GridRow和GridCol来实现栅格布局效果,
相对于目前已废弃的GridContainer组件,GridRow和GridCol提供了更灵活、更全面的栅格系统实现方案。GridRow为栅格容器组件,只能与栅格子组件GridCol在栅格布局场景中使用。
栅格容器GridRow
栅格容器有columns、gutter、direction、breakpoints四个属性。
- columns: 栅格布局的主要定位工具,设置栅格布局的总列数。
- gutter: 设置元素之间的距离,决定内容间的紧密程度。
- direction: 设置栅格子组件在栅格容器中的排列方向。
- breakpoints:以设备宽度为基准,将应用宽度分成了几个不同的区间,即不同的断点。开发者可根据需要在不同的区间下实现不同的页面布局效果。
首先通过设置断点,得到一系列断点区间;然后,借助栅格组件能力监听应用窗口大小的变化,判断应用当前处于哪个断点区间,最后调整应用的布局。
栅格系统断点
栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。
栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下:
断点名称 | 取值范围(vp) |
xs | [0, 320) |
sm | [320, 520) |
md | [520, 840) |
lg | [840, +∞) |
在GridRow新栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外,
还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。
断点名称 | 设备描述 |
xs | 最小宽度类型设备。 |
sm | 小宽度类型设备。 |
md | 中等宽度类型设备。 |
lg | 大宽度类型设备。 |
xl | 特大宽度类型设备。 |
xxl | 超大宽度类型设备。 |
- 针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。
breakpoints: {value: ["100vp", "200vp"]}
表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。
breakpoints: {value: ["320vp", "520vp", "840vp", "1080vp"]}
表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-520vp为sm,520vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。
- 栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。
下例中,使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。效果如图:
GridRow({
breakpoints: {
value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.bgColors, (color, index) => {
GridCol({
span: {
xs: 2,
sm: 3,
md: 4,
lg: 6,
xl: 8,
xxl: 12
}
}) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}.backgroundColor(color)
})
}
栅格布局的总列数
GridRow中通过columns设置栅格布局的总列数。
- columns默认值为12,当未设置columns时,在任何断点下,栅格布局被分成12列。
GridRow() {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
- 当columns类型为number时,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下:
Row() {
GridRow({ columns: 4 }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
.width("100%").height("100%")
.onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint
})
}
.height(160)
.border({ color: Color.Blue, width: 2 })
.width('90%')
Row() {
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
.width("100%").height("100%")
.onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint
})
}
.height(160)
.border({ color: Color.Blue, width: 2 })
.width('90%')
- 当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。
GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
- 如上,若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:8, md:10,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:10, xl:10, xxl:10。
栅格子组件间距
GridRow中通过gutter设置子元素在水平和垂直方向的间距。
- 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。
GridRow({ gutter: 10 }){}
- 当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。
GridRow({ gutter: { x: 20, y: 50 } }){}
排列方向
通过GridRow的direction属性设置栅格子组件在栅格容器中的排列方向。
- 子组件默认从左往右排列。
GridRow({ direction: GridRowDirection.Row }){}
- 子组件从右往左排列。
GridRow({ direction: GridRowDirection.RowReverse }){}
栅格子组件GridCol
GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span,offset,order的值。
- span的设置
GridCol({ span: 2 }){}
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.span(2)
GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 })
- offset的设置
GridCol({ offset: 2 }){}
GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){}
GridCol(){}.offset(2)
GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 })
- order的设置
GridCol({ order: 2 }){}
GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.order(2)
GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 })
下面使用传参的方式演示各属性的使用。
span
子组件占栅格布局的列数,决定了子组件的宽度,默认为1。
- 当类型为number时,子组件在所有尺寸设备下占用的列数相同。
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: 2 }) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
offset
栅格子组件相对于前一个子组件的偏移列数,默认为0。
- 当类型为number时,子组件偏移相同列数。
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: 2 }) {
Row() {
Text("" + index)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text("" + index)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
order
栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。
当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。
- 当类型为number时,子组件在任何尺寸下排序次序一致。
GridRow() {
GridCol({ order: 5 }) {
Row() {
Text("1")
}.width("100%").height("50vp")
}.backgroundColor(Color.Red)
GridCol({ order: 4 }) {
Row() {
Text("2")
}.width("100%").height("50vp")
}.backgroundColor(Color.Orange)
GridCol({ order: 3 }) {
Row() {
Text("3")
}.width("100%").height("50vp")
}.backgroundColor(Color.Yellow)
GridCol({ order: 2 }) {
Row() {
Text("4")
}.width("100%").height("50vp")
}.backgroundColor(Color.Green)
}
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。
GridRow() {
GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) {
Row() {
Text("1")
}.width("100%").height("50vp")
}.backgroundColor(Color.Red)
GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) {
Row() {
Text("2")
}.width("100%").height("50vp")
}.backgroundColor(Color.Orange)
GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) {
Row() {
Text("3")
}.width("100%").height("50vp")
}.backgroundColor(Color.Yellow)
GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) {
Row() {
Text("4")
}.width("100%").height("50vp")
}.backgroundColor(Color.Green)
}
媒体查询
媒体查询(Media Query)作为响应式设计的核心,在移动设备上应用十分广泛。它根据不同设备类型或同设备不同状态修改应用的样式。媒体查询的优势有:
- 提供丰富的媒体特征监听能力,针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
- 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
媒体查询引入与使用流程
媒体查询通过媒体查询接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:
首先导入媒体查询模块。
import mediaquery from '@ohos.mediaquery'
通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。
listener = mediaquery.matchMediaSync('(orientation: landscape)')
给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait)
媒体查询条件
媒体查询条件由媒体类型,逻辑操作符,媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用()包裹且可以有多个。具体规则如下:
语法规则
[media-type] [and|not|only] [(media-feature)]
例如:
screen and (round-screen: true) :当设备屏幕是圆形时条件成立。
(max-height: 800) :当高度小于等于800时条件成立。
(height <= 800) :当高度小于等于800时条件成立。
screen and (device-type: tv) or (resolution < 2) :包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。
媒体类型(media-type)
类型 | 说明 |
screen | 按屏幕相关参数进行媒体查询。 |
媒体逻辑操作(and|or|not|only)
媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。
表1 媒体逻辑操作符
类型 | 说明 |
and | 将多个媒体特征(Media Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。 例如:screen and (device-type: wearable) and (max-height: 600) 表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。 |
or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。 例如:screen and (max-height: 1000) or (round-screen:true)表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
not | 取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。 例如:not screen and (min-height: 50) and (max-height: 600) 表示当应用高度小于50个像素单位或者大于600个像素单位时成立。 使用not运算符时必须指定媒体类型。 |
only | 当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如: screen and (min-height: 50) 老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。 使用only时必须指定媒体类型。 |
, (comma) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。 例如:screen and (min-height: 1000), (round-screen:true) 表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
在MediaQuery Level 4中引入了范围查询,使其能够使用max-,min-的同时,也支持了< =,> =,< ,> 操作符。
表2 媒体逻辑范围操作符
类型 | 说明 |
< = | 小于等于,例如:screen and (height < = 50)。 |
> = | 大于等于,例如:screen and (height > = 600)。 |
< | 小于,例如:screen and (height < 50)。 |
> | 大于,例如:screen and (height > 600)。 |
媒体特征(media-feature)
类型 | 说明 |
height | 应用页面显示区域的高度。 |
min-height | 应用页面显示区域的最小高度。 |
max-height | 应用页面显示区域的最大高度。 |
width | 应用页面显示区域的宽度。 |
min-width | 应用页面显示区域的最小宽度。 |
max-width | 应用页面显示区域的最大宽度。 |
resolution | 设备的分辨率,支持dpi,dppx和dpcm单位。其中: - dpi表示每英寸中物理像素个数,1dpi≈0.39dpcm; - dpcm表示每厘米上的物理像素个数,1dpcm ≈ 2.54dpi; - dppx表示每个px中的物理像素数(此单位按96px=1英寸为基准,与页面中的px单位计算方式不同),1dppx = 96dpi。 |
min-resolution | 设备的最小分辨率。 |
max-resolution | 设备的最大分辨率。 |
orientation | 屏幕的方向。 可选值: - orientation: portrait(设备竖屏) - orientation: landscape(设备横屏) |
device-height | 设备的高度。 |
min-device-height | 设备的最小高度。 |
max-device-height | 设备的最大高度。 |
device-width | 设备的宽度。 |
device-type | 设备的类型。 可选值:default |
min-device-width | 设备的最小宽度。 |
max-device-width | 设备的最大宽度。 |
round-screen | 屏幕类型,圆形屏幕为true, 非圆形屏幕为 false。 |
dark-mode | 系统为深色模式时为true,否则为false。 |
场景示例
下例中使用媒体查询,实现屏幕横竖屏切换时给页面文本应用不同的内容和样式的效果。
import mediaquery from '@ohos.mediaquery'
let portraitFunc = null
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093'
@State text: string = 'Portrait'
listener = mediaquery.matchMediaSync('(orientation: landscape)') // 当设备横屏时条件成立
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
this.color = '#FFD700'
this.text = 'Landscape'
} else {
this.color = '#DB7093'
this.text = 'Portrait'
}
}
aboutToAppear() {
portraitFunc = this.onPortrait.bind(this) // 绑定当前应用实例
this.listener.on('change', portraitFunc)
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.text).fontSize(50).fontColor(this.color)
}
.width('100%').height('100%')
}
}
横屏下文本内容为Landscape,颜色为#FFD700。
非横屏下文本内容为Portrait,颜色为#DB7093。
文章转载自: