不同的屏幕尺寸,如何适配?鸿蒙提供了自适应布局和响应式布局。
名称 | 简介 |
---|---|
自适应布局 | 拉伸屏幕,页面的位置关系没有发生变化。自适应布局常常需要借助Row组件、Column组件或Flex组件实现。当前自适应布局能力有7种:拉伸能力、均分能力、占比能力、缩放能力、延伸能力、隐藏能力、折行能力。 |
响应式布局 | 拉伸屏幕,页面的位置关系发生变化。响应式布局常常与GridRow组件、List组件、Swiper组件或Tabs组件搭配使用。响应式布局能力有3种:断点、媒体查询、栅格布局。 |
自适应布局
下面介绍自适应布局的7种能力。自适应布局的7种能力需要牢记于心。
拉伸能力
父组件尺寸发生变化,增加或减小指定组件的尺寸。
属性 | 默认值 | 描述 |
---|---|---|
flexGrow | 0 | 父容器宽度大于所有子组件宽度的总和,子组件按照比例分配父容器的多余空间。 |
flexShrink | 1 | 父容器宽度小于所有子组件宽度的总和。子组件按照比例收缩分配父容器的不足空间。 |
flexBasis | 'auto' | 设置组件在Flex容器中主轴方向上基准尺寸。'auto'意味着使用组件原始的尺寸,不做修改。flexBasis属性不是必须的,通过width或height也可以达到同样的效果。当flexBasis属性与width或height发生冲突时,以flexBasis属性为准。 |
下面的示例中,页面由中间的图片以及两侧的留白区组成,各区域的属性配置如下:
- 中间内容区的宽度设置为400vp,同时将flexGrow属性设置为1,flexShrink属性设置为0。
- 两侧留白区的宽度设置为150vp,同时将flexGrow属性设置为0,flexShrink属性设置为1。
父容器的基准尺寸是700vp(150vp+400vp+150vp)。可以通过拖动底部的滑动条改变父容器的尺寸,查看布局变化。 - 当父容器的尺寸大于700vp时,父容器中多余的空间全部分配给中间内容区。
-
当父容器的尺寸小于700vp时,左右两侧的留白区按照“1:1”的比例收缩。
@Entry
@Component
struct FlexibleCapabilitySample1 {
@State containerWidth: number = 402
// 底部滑块,可以通过拖拽滑块改变容器尺寸。
@Builder slider() {
Slider({ value: this.containerWidth, min: 402, max: 1000, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.onChange((value: number) => {
this.containerWidth = value;
})
.position({ x: '20%', y: '80%' })
}
build() {
Column() {
Column() {
Row() {
// 通过flexGrow和flexShrink属性,将多余的空间全部分配给图片,将不足的控件全部分配给两侧空白区域。
Row().width(150).height(400).backgroundColor('#FFFFFF')
.flexGrow(0).flexShrink(1)
Image($r("app.media.illustrator")).width(400).height(400)
.objectFit(ImageFit.Contain)
.backgroundColor("#66F1CCB8")
.flexGrow(1).flexShrink(0)
Row().width(150).height(400).backgroundColor('#FFFFFF')
.flexGrow(0).flexShrink(1)
}
.width(this.containerWidth)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
}
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
如果期望将父容器的剩余空间全部分配给某空白区域时,也可以通过Blank组件实现。注意仅当父组件为Row、Column、Flex组件时,Blank组件才会生效。
@Entry
@Component
struct FlexibleCapabilitySample2 {
@State rate: number = 0.8
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: this.rate * 100, min: 30, max: 80, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.onChange((value: number) => {
this.rate = value / 100;
})
.position({ x: '20%', y: '80%' })
}
build() {
Column() {
Column() {
Row() {
Text('飞行模式')
.fontSize(16)
.width(135)
.height(22)
.fontWeight(FontWeight.Medium)
.lineHeight(22)
Blank() // 通过Blank组件实现拉伸能力
Toggle({ type: ToggleType.Switch })
.width(36)
.height(20)
}
.height(55)
.borderRadius(12)
.padding({ left: 13, right: 13 })
.backgroundColor('#FFFFFF')
.width(this.rate * 100 + '%')
}
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
均分能力
父容器缩放,子组件的尺寸不变,只是中间的间距或者留白按照比例缩放。均分能力可以通过将Row组件、Column组件或Flex组件的justifyContent属性设置为FlexAlign.SpaceEvenly实现。
占比能力
子组件的宽高按照预设的比例,随父容器组件发生变化。占比能力通常有两种实现方式:
- 将子组件的宽高设置为父组件宽高的百分比。
-
设置权重layoutWeight属性。
@Entry
@Component
struct ProportionCapabilitySample {
@State rate: number = 0.5
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: 100, min: 25, max: 50, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.rate = value / 100
})
.position({ x: '20%', y: '80%' })
}
build() {
Column() {
Column() {
Row() {
Column() {
Image($r("app.media.down"))
.width(48)
.height(48)
}
.height(96)
.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Image($r("app.media.pause"))
.width(48)
.height(48)
}
.height(96)
.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
.backgroundColor('#66F1CCB8')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column() {
Image($r("app.media.next"))
.width(48)
.height(48)
}
.height(96)
.layoutWeight(1) // 设置子组件在父容器主轴方向的布局权重
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width(this.rate * 100 + '%')
.height(96)
.borderRadius(16)
.backgroundColor('#FFFFFF')
}
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
缩放能力
设置aspectRatio宽高比属性,父容器发生改变,子组件的宽高比不变。
@Entry
@Component
struct ScaleCapabilitySample {
@State sliderWidth: number = 400
@State sliderHeight: number = 400
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: this.sliderHeight, min: 100, max: 400, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.sliderHeight = value
})
.position({ x: '20%', y: '80%' })
Slider({ value: this.sliderWidth, min: 100, max: 400, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.sliderWidth = value;
})
.position({ x: '20%', y: '87%' })
}
build() {
Column() {
Column() {
Column() {
Image($r("app.media.illustrator")).width('100%').height('100%')
}
.aspectRatio(1) // 固定宽高比
.border({ width: 2, color: "#66F1CCB8"}) // 边框,仅用于展示效果
}
.backgroundColor("#FFFFFF")
.height(this.sliderHeight)
.width(this.sliderWidth)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor("#F1F3F5")
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
延伸能力
子组件随着父容器尺寸变化显示或者隐藏。延伸能力通常有两种实现方式:
- 通过List组件。
-
通过Scroll组件配合Row组件或Column组件实现。
@Entry
@Component
struct ExtensionCapabilitySample1 {
@State rate: number = 0.60
readonly appList: number [] = [0, 1, 2, 3, 4, 5, 6, 7]
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: this.rate * 100, min: 8, max: 60, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.rate = value / 100
})
.position({ x: '20%', y: '80%' })
}
build() {
Column() {
Row({ space: 10 }) {
// 通过List组件实现隐藏能力
List({ space: 10 }) {
ForEach(this.appList, (item:number) => {
ListItem() {
Column() {
Image($r("app.media.startIcon")).width(48).height(48).margin({ top: 8 })
Text('App name')
.width(64)
.height(30)
.lineHeight(15)
.fontSize(12)
.textAlign(TextAlign.Center)
.margin({ top: 8 })
.padding({ bottom: 15 })
}.width(80).height(102)
}.width(80).height(102)
})
}
.padding({ top: 16, left: 10 })
.listDirection(Axis.Horizontal)
.width('100%')
.height(118)
.borderRadius(16)
.backgroundColor(Color.White)
}
.width(this.rate * 100 + '%')
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
隐藏能力
给子组件设置布局优先级(displayPriority属性),父组件尺寸变化,按照优先级对子组件进行显示或者隐藏。
@Entry
@Component
struct HiddenCapabilitySample {
@State rate: number = 0.45
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: this.rate * 100, min: 10, max: 45, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.height(50)
.onChange((value: number) => {
this.rate = value / 100
})
.position({ x: '20%', y: '80%' })
}
build() {
Column() {
Row() {
Image($r("app.media.favorite"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.margin({ left: 12, right: 12 })
.displayPriority(1) // 布局优先级
Image($r("app.media.down"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.margin({ left: 12, right: 12 })
.displayPriority(2) // 布局优先级
Image($r("app.media.pause"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.margin({ left: 12, right: 12 })
.displayPriority(3) // 布局优先级
Image($r("app.media.next"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.margin({ left: 12, right: 12 })
.displayPriority(2) // 布局优先级
Image($r("app.media.list"))
.width(48)
.height(48)
.objectFit(ImageFit.Contain)
.margin({ left: 12, right: 12 })
.displayPriority(1) // 布局优先级
}
.width(this.rate * 100 + '%')
.height(96)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
this.slider()
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
折行能力
折行能力通过使用 Flex折行布局 (将wrap属性设置为FlexWrap.Wrap)实现,当横向布局尺寸不足以完整显示内容元素时,通过折行的方式,将元素显示在下方。
@Entry
@Component
struct WrapCapabilitySample {
@State rate: number = 0.7
readonly imageList: Resource [] = [
$r('app.media.flexWrap1'),
$r('app.media.flexWrap2'),
$r('app.media.flexWrap3'),
$r('app.media.flexWrap4'),
$r('app.media.flexWrap5'),
$r('app.media.flexWrap6')
]
// 底部滑块,可以通过拖拽滑块改变容器尺寸
@Builder slider() {
Slider({ value: this.rate * 100, min: 50, max: 70, style: SliderStyle.OutSet })
.blockColor(Color.White)
.width('60%')
.onChange((value: number) => {
this.rate = value / 100
})
.position({ x: '20%', y: '87%' })
}
build() {
Flex({ justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
Column() {
// 通过Flex组件warp参数实现自适应折行
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center,
wrap: FlexWrap.Wrap
}) {
ForEach(this.imageList, (item:Resource) => {
Image(item).width(183).height(138).padding(10)
})
}
.backgroundColor('#FFFFFF')
.padding(20)
.width(this.rate * 100 + '%')
.borderRadius(16)
}
.width('100%')
this.slider()
}.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
这就是自适应布局的7种能力,下面给出一个案例,主要是想让大家知道这7种能力可以用在什么地方。
上图是一个音乐播放器,左边是音乐播放器在平板上的显示效果,中间是音乐播放器在手机上的显示效果,右边是音乐播放器在折叠屏上的显示效果。我们把音乐播放器分为6个区域。
区域 | 布局能力 | 实现方案 |
---|---|---|
1、标题栏 | 自适应布局-拉伸能力 | 外层使用Row组件,内层的留白组件自带拉伸能力。 |
2、专辑图片 | 自适应布局-缩放能力 | 设置图片aspectRatio属性,将宽高比设置1:1。 |
3、收藏/下载/评论/分享 | 自适应布局-均分能力 | justifyContent属性设置为FlexAlign.SpaceEvenly。 |
4、底部播放量 | 自适应布局-占比能力 | 设置layoutWeight属性,将左侧与右侧占比为3:1。 |
5、收藏/播放/上一首/下一首 | 自适应布局-隐藏能力 | 设置优先级displayPriority属性,平板显示5个按钮,折叠屏显示3个按钮,手机显示一个按钮。 |
6、音乐列表 | 自适应布局-延伸能力 | 设置lanes,列表显示1列或者两列。 |
响应式布局
拉伸屏幕,页面的位置关系发生变化。自适应布局可以保证窗口尺寸在一定范围内变化时,页面的显示是正常的。但是将窗口尺寸变化较大时(如窗口宽度从400vp变化为1000vp),仅仅依靠自适应布局可能出现图片异常放大或页面内容稀疏、留白过多等问题,此时就需要借助响应式布局能力调整页面结构。
断点
将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。断点支持自定义,取值范围可以修改,下标是4个常见断点范围。
名称 | 取值范围 |
---|---|
xs(超小,智能穿戴类设备) | [0, 320) |
sm(小,手机) | [320, 600) |
xs(中等,折叠屏) | [600, 840) |
xs(大,平板) | [840, +∞) |
可以根据实际需要在lg断点后面新增xl、xxl等断点,但注意新增断点会同时增加设计师及开发者的工作量。
系统提供了多种方法,判断应用当前处于何种断点,进而可以调整应用的布局。先介绍如何通过窗口对象监听断点变化。
在UIAbility的onWindowStageCreate生命周期回调中,通过窗口对象获取启动时的应用窗口宽度并注册回调函数监听窗口尺寸变化。将窗口尺寸的长度单位由px换算为vp后,即可基于前文中介绍的规则得到当前断点值,此时可以使用状态变量记录当前的断点值方便后续使用。
// MainAbility.ts
import window from '@ohos.window'
import display from '@ohos.display'
import UIAbility from '@ohos.app.ability.UIAbility'
export default class MainAbility extends UIAbility {
private windowObj?: window.Window
private curBp: string = ''
//...
// 根据当前窗口尺寸更新断点
private updateBreakpoint(windowWidth: number) :void{
// 将长度的单位由px换算为vp
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().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)
}
}
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)
})
});
// ...
}
//...
}
在页面中,获取及使用当前的断点。
@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%')
}
}
媒体查询
媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等,因此在应用开发过程中使用的非常广泛。下面通过通过媒体查询,监听应用窗口宽度变化,获取当前应用所处的断点值。
export class BreakpointSystem {
private currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM;
// 监听sm的屏幕尺寸
private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_SM);
// 监听md的屏幕尺寸
private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_MD);
// 监听lg的屏幕尺寸
private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_LG);
private updateCurrentBreakpoint(breakpoint: string): void {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint;
// 将断点保存到AppStorage
AppStorage.setOrCreate<string>(BreakpointConstants.CURRENT_BREAKPOINT, this.currentBreakpoint);
}
}
private isBreakpointSM = (mediaQueryResult: mediaquery.MediaQueryResult): void => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_SM);
}
}
private isBreakpointMD = (mediaQueryResult: mediaquery.MediaQueryResult): void => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_MD);
}
}
private isBreakpointLG = (mediaQueryResult: mediaquery.MediaQueryResult): void => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_LG);
}
}
public register(): void {
this.smListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_SM);
this.smListener.on('change', this.isBreakpointSM);
this.mdListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_MD);
this.mdListener.on('change', this.isBreakpointMD);
this.lgListener = mediaquery.matchMediaSync(BreakpointConstants.RANGE_LG);
this.lgListener.on('change', this.isBreakpointLG);
}
public unregister(): void {
this.smListener.off('change', this.isBreakpointSM);
this.mdListener.off('change', this.isBreakpointMD);
this.lgListener.off('change', this.isBreakpointLG);
}
}
在上述代码中,我们定义不同的屏幕尺寸监听,通过媒体查询mediaquery.matchMediaSync来监听屏幕尺寸。将监听到的屏幕尺寸保存AppStorage,这样其它页面就能通过AppStorage获取屏幕尺寸。同时提供注册register方法和注销unregister方法。
@Entry
@Component
struct MediaQuerySample {
@StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
private breakpointSystem: BreakpointSystem = new BreakpointSystem()
aboutToAppear() {
this.breakpointSystem.register()
}
aboutToDisappear() {
this.breakpointSystem.unregister()
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.currentBreakpoint)
.fontSize(24)
.margin(10)
}
.width('100%')
.height('100%')
}
}
在上述代码中,在aboutToAppear中注册媒体查询,在aboutToDisappear中注销媒体查询。由于断点保存在AppStorage,所以可以直接使用@StorageLink装饰器从AppStorage中取出断点。
栅格布局
根据设备的宽度,将不同的屏幕尺寸划分为不同数量的栅格,来实现屏幕的自适应。如下图,小尺寸的手机可以画4个栅格,折叠屏可以画8个栅格,平板可以画12个栅格。一般来说,推荐按照4、8、12的比例进行栅格划分。栅格和栅格之前有12vp的间距,如果没有间距,栅格就会挤在一起。
span用于设置栅格的数量,offset用于设置偏移量。如下图,手机设置4个栅格,不设置偏移量。折叠屏总共有8个栅格,设置6个栅格,偏移1个栅格,就达到了居中的效果。平板总共有12个栅格,设置8个栅格,偏移2个栅格,就达到了居中的效果。
下面的代码就实现了上面所说的在不同设备上的登录页面。
build() {
GridRow({
/**
* columns用于指定不同设备占据的总栅格数,默认情况下,总栅格数为12
* 指定手机的总栅格数为4,折叠屏总栅格数为8,平板总栅格数为12。
*/
columns:{sm: 4, md: 8, lg: 12},
// 间距
gutter: 12
}) {
// 子组件
GridCol({
// 手机占4个栅格,折叠屏占8个栅,平板占12个栅格。
span: {sm: 4, md: 6, lg: 8},
// 手机不偏移,折叠屏偏移一个栅格,平板偏移2个栅格。
offset: {sm: 0, md: 1, lg: 2}
}) {
// 登录页面
this.loginUI()
}
}
}
栅格组件提供了丰富的自定义能力,功能异常灵活和强大。只需要明确栅格在不同断点下的Columns、Margin、Gutter及span等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。以上只是简单的介绍了下栅格布局,估计有人没看懂,关于栅格布局的详细文档还请查看官方文档。