跳到主要内容

第十一章:一多开发(一次开发,多端部署)

本章配套代码:code/Chapter11(栅格布局 / 媒体查询 / 元素自适应 / 新闻资讯多端实训 Demo) 前置要求:已完成基础布局与状态管理章节 参考:教材《一次开发多端部署》第 11 章(多设备界面开发 + 技能实训)

11.1 学习目标

  • 理解"一次开发,多端部署"(一多)理念与三大关键能力
  • 认识多设备窗口形态(手机 / 平板 / 折叠屏 / 智慧屏)与布局的关系
  • 掌握栅格布局(Grid)配合真实窗口宽度实现列数自适应
  • 掌握媒体查询(MediaQuery)在断点处自动切换布局
  • 掌握界面元素自适应(layoutWeight / 图片缩放 / constraintSize)
  • 通过"新闻资讯"多端实训综合运用上述能力

11.2 一多开发概述

HarmonyOS 面向"1+8+N"全场景设备生态提出"一次开发、多端部署"能力:开发者只需编写一套代码,即可让应用在手机、平板、折叠屏、智慧屏、车机、手表等终端上运行,并根据设备差异自动进行布局与能力适配

┌────────────────────────────────────────┐
│ 一次开发,多端部署 │
│ │
│ 手机(360vp) 平板(840vp) 折叠屏 │
│ │ │ │ │
│ └──── 同一套代码,按断点适配 ────┘ │
│ ├─ 窗口/屏幕形态适配 │
│ ├─ 布局响应式 + 元素自适应 │
│ └─ 终端能力差异兼容适配 │
└────────────────────────────────────────┘

三大关键能力:

能力说明
窗口与屏幕形态适配明确应用运行的设备形态,据此设计布局
布局响应式 + 元素自适应布局随屏幕宽度变化自动重排,元素随尺寸自适应
终端能力差异兼容适配能力缺失时降级或提示,资源按需加载

💡 自适应单位 vp / fp:ArkUI 推荐使用 vp(虚拟像素,随屏幕密度自适应,用于尺寸与间距)和 fp(字体像素,随字体设置自适应,用于文字字号),避免写死 px

11.3 多设备窗口形态

不同形态设备的屏幕宽窄、横竖屏取向均不同,布局也应随之不同。

设备形态屏幕特点布局建议
手机窄屏、竖屏为主(约 320~420vp)单栏纵向
平板宽屏、横竖可切换多栏布局
折叠屏展开 / 折叠双形态展开双栏、折叠单栏
智慧屏宽屏、横向为主统一多栏大屏布局

🔑 设计界面时应禁止写死宽度,优先采用"列数可自适应变化"的栅格化布局,让界面随屏幕宽度自动调整内容列数。

11.4 栅格响应式布局(Grid + 真实窗口宽度)

11.4.1 核心思路

栅格布局将界面划分为若干"列",由元素占列(GridItem)排布。通过切换列数实现单栏 / 多栏自动变换:窄屏少列、宽屏多列。

本课程不手动模拟宽度,而是通过 display.getDefaultDisplaySync() 读取真实窗口宽度,再用 px2vp() 换算成 vp 后决定列数:

import { display } from '@kit.ArkUI'

@ComponentV2
struct GridLayoutDemo {
@Local columns: number = 1
@Local windowWidth: number = 0
@Local simWide: boolean = false

aboutToAppear(): void {
// ① 读取真实窗口宽度并换算为 vp
this.windowWidth = Math.floor(px2vp(display.getDefaultDisplaySync().width))
this.applyColumns()
}

private applyColumns(): void {
// ② 宽屏(≥600vp) 2 列,窄屏 1 列
this.columns = this.simWide ? 2 : (this.windowWidth >= 600 ? 2 : 1)
}

build() {
Column({ space: 12 }) {
Text(`当前窗口宽度: ${this.windowWidth}vp 列数: ${this.columns}`)
.fontSize(16)
.fontColor('#0A59F7')

Grid() {
ForEach([1, 2, 3, 4, 5, 6], (n: number) => {
GridItem() {
Text(`项目 ${n}`)
.width('100%')
.height(120)
.textAlign(TextAlign.Center)
.fontSize(20)
.fontColor(Color.White)
.backgroundColor(n % 2 === 0 ? '#1890FF' : '#0A59F7')
}
}, (n: number) => n.toString())
}
.columnsTemplate(`repeat(${this.columns}, 1fr)`) // ③ 列数动态化
.columnsGap(8)
.rowsGap(8)
.padding(8)
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#f5f6fa')
}
}

关键点:

  • display.getDefaultDisplaySync().width 获取当前窗口宽度(单位 px)
  • px2vp() 将 px 换算为 vp,与设计稿一致
  • columnsTemplate('repeat(N, 1fr)')N 是状态变量,列数变化时 Grid 自动重排
  • 手机上可通过"模拟宽屏"按钮强制 2 列,演示宽屏效果(用于手机模拟器验证)

💡 repeat(2, 1fr) 表示生成 2 个等宽列。也可混用固定与弹性:'100px 1fr 2fr'

11.5 媒体查询断点(MediaQuery)

11.5.1 基本用法

媒体查询用于监听屏幕宽度、方向、色彩等条件,条件变化时触发回调,动态调整布局:

import { mediaquery } from '@kit.ArkUI'

@ComponentV2
struct MediaQueryDemo {
@Local isWide: boolean = false
@Local breakpoint: string = 'sm (窄屏 <600vp)'
@Local simWide: boolean | null = null
private mq: mediaquery.MediaQueryListener | null = null

aboutToAppear(): void {
// ① 创建媒体查询监听器(宽度 ≥ 600vp)
this.mq = mediaquery.matchMediaSync('(width >= 600vp)')
// ② 注册变更回调:条件满足时自动切换
this.mq.on('change', (result: mediaquery.MediaQueryResult) => {
this.applyState(result.matches)
})
// ③ 首次立即应用一次
this.applyState(this.mq.matches)
}

aboutToDisappear(): void {
this.mq?.off('change') // ④ 注销监听,避免内存泄漏
}

private applyState(matches: boolean): void {
if (this.simWide !== null) {
this.isWide = this.simWide
this.breakpoint = this.simWide ? 'lg (模拟宽屏 ≥600vp)' : 'sm (模拟窄屏 <600vp)'
} else {
this.isWide = matches
this.breakpoint = matches ? 'lg (宽屏 ≥600vp)' : 'sm (窄屏 <600vp)'
}
}

build() {
Column({ space: 12 }) {
Text(`当前断点: ${this.breakpoint}`)
.fontSize(16)
.fontColor('#0A59F7')

if (this.isWide) {
Row({ space: 10 }) { // 宽屏:双栏
Column() { Text('左栏') }
.width('50%').height(300)
.justifyContent(FlexAlign.Center)
.backgroundColor('#0A59F7').borderRadius(12)
Column() { Text('右栏') }
.width('50%').height(300)
.justifyContent(FlexAlign.Center)
.backgroundColor('#1890FF').borderRadius(12)
}
.width('100%')
} else {
Column() { Text('单栏') } // 窄屏:单栏
.width('100%').height(300)
.justifyContent(FlexAlign.Center)
.backgroundColor('#0A59F7').borderRadius(12)
}
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#f5f6fa')
}
}

媒体查询常用条件:

查询示例说明
(width >= 600vp)宽度断点
(orientation: landscape)横屏
(prefers-color-scheme: dark)深色模式
(device-type: tablet)设备类型

11.5.2 断点体系

断点宽度说明
xs<320vp极小屏
sm320-600vp手机
md600-840vp折叠屏 / 小平板
lg>840vp平板

🔑 宽屏 = 多栏(Row 并排),窄屏 = 单栏(Column 纵向),媒体查询在断点处自动切换,无需手动触发。

11.6 界面元素自适应

除了整体布局响应式,单个元素(图片、文字、按钮、间距)也要随屏幕尺寸自适应,窄屏不溢出、宽屏充分利用空间。

11.6.1 layoutWeight 按比例分配空间

父容器尺寸变化时,子组件自动按权重比例分配剩余空间:

Row() {
Text('自适应A').layoutWeight(1).height(60)...
Text('自适应B').layoutWeight(2).height(60)...
Text('自适应C').layoutWeight(3).height(60)...
}
.width('100%')

A:B:C = 1:2:3,宽度变化时自动按比例伸缩。

11.6.2 图片自适应(百分比宽度 + 等比缩放)

为图片设置百分比宽度,配合 aspectRatioobjectFit 等比缩放,避免变形或溢出:

Image($r('app.media.startIcon'))
.width(this.imageWidth + '%') // 百分比宽度
.aspectRatio(1) // 宽高比 1:1
.objectFit(ImageFit.ScaleDown) // 等比缩小,不裁剪不拉伸
.borderRadius(12)

11.6.3 constraintSize 弹性约束

限制组件最大 / 最小尺寸,元素既能自适应增减又不会无限放大缩小:

Text('约束区域')
.constraintSize({ minWidth: 80, maxWidth: 150, minHeight: 80, maxHeight: 150 })
.width('100%')
.height(120)
.backgroundColor('#722ED1')

💡 constraintSize 是较稳妥的自适应方案:设置 width('100%') 跟随父容器,同时用约束兜底尺寸上限。

11.7 综合实训:新闻资讯多端

对应 code/Chapter11Index.ets,模拟教材"新闻资讯"技能实训,将栅格 + 媒体查询 + 元素自适应综合应用:手机窄屏单栏列表,平板/折叠屏展开态双栏展示,并在界面展示当前断点与列数便于调试。

import { mediaquery } from '@kit.ArkUI'

interface NewsItem {
title: string
summary: string
color: string
}

@ComponentV2
struct NewsReaderDemo {
@Local news: NewsItem[] = []
@Local columns: number = 1
@Local breakpoint: string = 'sm (窄屏 <600vp)'
@Local simWide: boolean | null = null
private mq: mediaquery.MediaQueryListener | null = null

aboutToAppear(): void {
this.loadNews()
this.mq = mediaquery.matchMediaSync('(width >= 600vp)')
this.mq.on('change', (res: mediaquery.MediaQueryResult) => {
this.applyState(res.matches)
})
this.applyState(this.mq.matches)
}

aboutToDisappear(): void {
this.mq?.off('change')
}

private applyState(matches: boolean): void {
if (this.simWide !== null) {
this.columns = this.simWide ? 2 : 1
this.breakpoint = this.simWide ? 'lg (模拟宽屏 ≥600vp)' : 'sm (模拟窄屏 <600vp)'
} else {
this.columns = matches ? 2 : 1
this.breakpoint = matches ? 'lg (宽屏 ≥600vp)' : 'sm (窄屏 <600vp)'
}
}

private loadNews(): void {
this.news = [
{ title: '华为发布新旗舰', summary: '全面提升影像与续航体验,多摄系统带来专业级拍摄表现。', color: '#0A59F7' },
{ title: 'HarmonyOS 生态', summary: '一次开发多端部署成为焦点,跨设备协同体验全面升级。', color: '#1890FF' },
{ title: '折叠屏新形态', summary: '展开大屏高效办公,折叠便携出行,双形态自由切换。', color: '#722ED1' },
{ title: '多端适配实践', summary: '栅格布局与媒体查询结合,一套代码适配手机与平板。', color: '#52C41A' },
{ title: '开发者大会预告', summary: '下一代 ArkUI 能力即将发布,响应式布局持续进化。', color: '#FA8C16' },
{ title: '智慧屏新场景', summary: '大屏多栏布局带来沉浸式家庭娱乐体验,交互更自然。', color: '#E74C3C' }
]
}

build() {
Column({ space: 8 }) {
Text('新闻资讯')
.fontSize(22)
.fontWeight(FontWeight.Bold)

Text(`当前断点: ${this.breakpoint} 列数: ${this.columns}`)
.fontSize(14)
.fontColor('#0A59F7')

Grid() {
ForEach(this.news, (item: NewsItem) => {
GridItem() {
Column({ space: 6 }) {
Text(item.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.maxLines(1)
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
}
.alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(Color.White)
.borderRadius(10)
.border({ width: 4, color: item.color })
}
}, (item: NewsItem) => item.title)
}
.columnsTemplate(`repeat(${this.columns}, 1fr)`)
.columnsGap(8)
.rowsGap(8)
.padding(8)
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#f5f6fa')
}
}

页面导航结构(对应 code/Chapter11Index.ets):

Index(Navigation 菜单页)
├── ① 栅格响应式布局(GridLayoutDemo)
├── ② 媒体查询断点(MediaQueryDemo)
├── ③ 元素自适应(ElementAdaptDemo)
└── ④ 新闻资讯多端实训(NewsReaderDemo)

四个 Demo 均作为 NavDestination 子页面,经 @Builder PageMap 映射跳转(Navigation 自动提供返回箭头与转场动画),与本课程第 6 章的导航写法完全一致。

运行效果(模拟器/Previewer):

  • 首页四个菜单项可分别进入各 Demo 页面
  • 栅格 Demo:显示真实窗口宽度与列数,手机 1 列 / 宽屏 2 列
  • 媒体查询 Demo:断点处自动切换单/双栏,实时显示当前断点
  • 元素自适应 Demo:layoutWeight 比例分配、图片缩放、约束区域
  • 新闻实训:手机单栏列表 → 平板/折叠屏展开态双栏卡片

运行效果截图:

Chapter11 一多开发总览

栅格响应式布局 Demo(窄屏,当前窗口 358vp,1 列)

栅格响应式布局 Demo(模拟宽屏,2 列)

媒体查询断点 Demo(窄屏断点 sm,单栏)

媒体查询断点 Demo(模拟宽屏断点 lg,双栏)

元素自适应 Demo(layoutWeight 比例分配 / 图片缩放 / 约束区域)

新闻资讯多端实训(窄屏断点 sm,1 列列表)

新闻资讯多端实训(模拟宽屏断点 lg,2 列卡片)

11.8 特殊界面布局场景

除常规手机与平板外,折叠屏、横竖屏切换等场景需要针对性适配。

11.8.1 折叠屏展开态 / 折叠态

监听折叠状态,展开态多栏、折叠态单栏:

import { display } from '@kit.ArkUI'

@ComponentV2
struct FoldDemo {
@Local isFold: boolean = false

aboutToAppear(): void {
display.getCurrentFoldStatus().then((state: display.FoldStatus) => {
this.isFold = (state === display.FoldStatus.FOLD)
})
}

build() {
if (this.isFold) {
Text('折叠态:单栏布局')
} else {
Row() { Text('左栏'); Text('右栏') }
}
}
}

11.8.2 横竖屏切换

设备方向变化时,通过监听屏幕方向(display.on('orientationChange'))或媒体查询宽度条件,重新调整布局。断点方案((width >= 600vp))天然覆盖横竖屏:横屏宽度增大自动切多栏。

11.9 常见问题

Q:如何测试不同屏幕? DevEco Studio 可创建不同尺寸模拟器,或用 Previewer 切换设备类型;本章 Demo 内置"模拟宽屏"按钮,可在手机上直接预览宽屏效果。

Q:display.getDefaultDisplaySync().width 返回的是什么单位? 返回 px(物理像素),需用 px2vp() 换算成 vp 再与断点比较。

Q:栅格、媒体查询、断点三者的关系? 三者配合:栅格(Grid)负责"列数可变的容器",媒体查询负责"监听条件自动触发",断点(600vp 等)是划分窄/宽屏的阈值。媒体查询触发后修改列数状态,Grid 自动重排。

Q:元素自适应只用 layoutWeight 够吗? 基础场景够用。图片用百分比宽度 + objectFit,尺寸受限用 constraintSize,多端组合使用更稳妥。

Q:如何适配刘海屏/挖孔屏? 使用安全区域 API(expandSafeArea)与系统提供的避让。

Q:折叠屏开合时布局如何变化? 监听 display.getCurrentFoldStatus() 或使用断点自动响应宽度变化。

11.10 本章小结

知识点说明
一多理念一次开发、多端部署,三大关键能力
窗口形态手机单栏 / 平板多栏 / 折叠屏双形态
栅格响应式Grid + 真实窗口宽度 + 动态列数
媒体查询matchMediaSync 断点自动切换
元素自适应layoutWeight / 图片缩放 / constraintSize
特殊场景折叠屏、横竖屏切换
综合实训新闻资讯单/双栏

11.11 课后练习

  1. display + px2vp 实现响应式新闻列表:窄屏单列,宽屏双列
  2. 用 Grid 实现 4→8 列自适应图标网格(按窗口宽度切换)
  3. 实现深色模式切换(媒体查询监听 prefers-color-scheme
  4. constraintSize 给头像设置弹性尺寸范围
  5. 设计一个宽屏显示侧边栏、窄屏隐藏侧边栏的页面
  6. 将练习整合进 Chapter11/Index.ets

11.12 参考资料

评论

加载中…
加载中...