都 2025 了,手机和平板还要各写一套 UI?ArkUI 一套自适应不香吗?
《零基础学鸿蒙:自适应布局实战指南》摘要:本文为鸿蒙开发者的实战笔记,重点介绍了ArkUI的响应式布局方案。从基础布局容器Row/Column的使用技巧开始,详细讲解了如何通过Flex属性实现弹性伸缩布局,以及利用Grid构建网格系统。文章包含可直接套用的ArkTS代码示例,并分享了工程实践中的自适应布局经验,帮助开发者在不同设备尺寸上实现优雅适配。内容涵盖页面骨架搭建、Flex卡片瀑布流实现和G
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
我是真心受够了那种“手机一份布局、Pad 再来一份、横竖屏各再修修补补”的循环加班。后来把 ArkUI 的布局容器、Flex/Grid 和设备自适配这一套认真啃完,我的页面结构一下“通了气”,同一套代码在手机、平板、甚至可折叠设备上都能优雅伸缩。
这篇就按你给的大纲来:布局容器 → Flex/Grid → 响应式布局方案。技术点我会围绕 Row/Column、Flex 属性、deviceType 适配,并给出可直接照抄的 ArkTS/ArkUI 代码。别担心,我会把“工程里真正有用的细节”都点明白,顺便加点“半夜改 UI 的心路历程”的吐槽,提神不伤身。
一、布局容器:Row / Column 的“家常菜”,但要会“调味”
先把基础打牢:ArkUI 的布局容器以 Row、Column、Stack、Flex、Grid 为核心。Row/Column 是最轻最稳的日常主力。
1.1 Row / Column 的常用心法
- Row:横向布局,常用于工具条、卡片行、标签行。
- Column:纵向布局,页面骨架、列表项、弹窗内容。
- Stack:叠放,徽标、悬浮按钮、角标最常用。
- 优先用 Row/Column 搭骨架,Flex/Grid 负责“灵活度和密度”。
一个标准页面骨架(标题 + 筛选条 + 内容 + 底部安全区):
@Entry
@Component
struct AdaptiveScaffold {
@State title: string = 'Dashboard'
@State filtersVisible: boolean = true
build() {
Column({ space: 12 }) {
// 顶栏
Row() {
Text(this.title).fontSize(22).fontWeight(FontWeight.Bold)
Blank() // 占位,让右侧按钮贴边
Button(this.filtersVisible ? 'Hide Filters' : 'Show Filters')
.onClick(() => this.filtersVisible = !this.filtersVisible)
}.height(56).padding({ left: 16, right: 16 })
// 筛选条(可折叠)
if (this.filtersVisible) {
Row({ space: 12 }) {
Text('Keyword').width(80)
TextInput({ placeholder: 'Search…' }).layoutWeight(1)
Button('Apply')
}.padding(16)
}
// 内容区
Column() {
// 待会儿放 Flex/Grid 的自适应内容
ContentPanel()
}.layoutWeight(1)
// 底部安全区
Row() {
Text('© 2025 Example, Inc.').fontSize(12).opacity(0.6)
}.height(40).padding({ left: 16, right: 16 })
}
.padding({ top: 12, bottom: 12 })
.width('100%').height('100%')
}
}
小技巧
.layoutWeight(1)可让某一块吃满剩余空间,特别适合内容区域。Blank()是个好东西,简单拉开左右端。- 把“边距、间距”抽成常量/主题 Token,后期改版省心。
二、Flex/Grid:当页面需要“更灵活”和“更高密度”
Row/Column 是家常便饭;Flex 和 Grid 是让你在不同尺寸里优雅换姿势的绝招。
2.1 Flex:从“盒子挤挤挨挨”到“会呼吸的行列”
容器属性(常用)
.direction(FlexDirection.Row|Column):主轴方向.wrap(FlexWrap.Wrap|NoWrap):是否换行.justifyContent(FlexAlign.Start|End|Center|SpaceBetween|SpaceAround|SpaceEvenly).alignItems(ItemAlign.Start|Center|End|Stretch).alignContent(FlexAlign.Start|SpaceBetween|...)(多行时整行对齐)
子项属性(常用)
.flexGrow(n):主轴方向扩张.flexShrink(n):主轴方向收缩.flexBasis(length):主轴初始尺寸.alignSelf(FlexAlignSelf.Start|Center|End|Stretch):覆盖容器 align
示例:带换行的卡片瀑布(轻量版)
@Component
struct ProductCard {
title: string
price: string
build() {
Column({ space: 6 }) {
// 这里用占位图示意
Stack() {
Rect().fill(Color.Grey).height(96).width('100%').borderRadius(12)
Text('IMG').fontSize(12).opacity(0.4)
}
Text(this.title).maxLines(2).fontSize(14)
Text(this.price).fontColor(Color.Red).fontWeight(FontWeight.Medium)
}
.padding(12)
.borderRadius(12)
.backgroundColor('#F7F7F7')
}
}
@Component
struct FlexGallery {
@State items: Array<{ title: string, price: string }> = new Array(20).fill(0).map((_, i) => ({
title: `Gadget #${i + 1}`, price: `$ ${(i + 1) * 3}.99`
}))
build() {
// 容器:横向、允许换行、行间距
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
ForEach(this.items, (it, i) => {
// 子项:设定“最小宽度 + 自适应扩张”
Column() {
ProductCard({ title: it.title, price: it.price })
}
.width('48%') // 小屏两列(简单粗暴),后面我们用响应式自动算
.flexGrow(1) // 允许扩张
.margin({ bottom: 12, right: 8, left: 8 })
}, it => it.title)
}
.justifyContent(FlexAlign.SpaceBetween)
}
}
理解要点
- Flex 适合“不规则宽高 + 行列会自动换行”的场景,心态上把它当“自适应行列流”。
- 注意
.flexBasis()与.width()的组合,width 定边界,flex 决定伸缩。 - 复杂网格(固定列数、跨行列)还是交给 Grid 更稳。
2.2 Grid:当你需要“正儿八经的网格系统”
ArkUI 的 Grid 更接近 CSS Grid 的思路:模板定义列/行、间距、网格项。最稳的“卡片宫格、仪表盘、多列信息流”,用它就完事。
容器属性(核心)
.columnsTemplate('1fr 1fr 1fr'):三等分列.rowsTemplate('auto'):行高自动.columnsGap(12) / .rowsGap(12):网格缝隙.edgeEffect(EdgeEffect.None):滚动边缘效果(可选)
Grid + GridItem 示例:
@Component
struct DashboardGrid {
@State cols: number = 2 // 默认两列,小屏友好
private columnsTemplateFor(n: number): string {
// 生成 "1fr 1fr ..." 模板字符串
return new Array(n).fill('1fr').join(' ')
}
build() {
Grid() {
// 12 个假数据模块
ForEach(new Array(12).fill(0).map((_, i) => i), (i: number) => {
GridItem() {
Column() {
Text(`Module #${i + 1}`).fontWeight(FontWeight.Medium)
Text('some metrics…').opacity(0.6).fontSize(12)
Spacer()
// 这里可以放图表、统计卡、微交互
}
.padding(16)
.height(120)
.borderRadius(16)
.backgroundColor('#FAFAFA')
}
})
}
.columnsTemplate(this.columnsTemplateFor(this.cols))
.rowsTemplate('auto')
.columnsGap(12)
.rowsGap(12)
.padding(12)
}
}
Grid VS Flex 怎么选?
- 固定列数/需要跨行跨列/信息密度高 → 用 Grid。
- 尺寸弹性大/希望自然换行/项宽不一致 → 用 Flex。
- 绝大多数仪表盘、宫格、商详 SKU 区、媒体卡板 → Grid 更可控。
三、响应式布局方案:尺寸、方向、设备类型“三板斧”
真正的“自适应”,不是把
.width('48%')改成.width('33%')就完了。要同时考虑:
- 屏幕宽度断点(breakpoints);2) 横竖屏;3) 设备类型(deviceType)。
我们来做一套通用响应式工具,让页面自动选择列数、边距、布局形态。
3.1 拿到屏幕信息 & 设备类型
@ohos.display获取宽高与像素密度;@ohos.deviceInfo获取deviceType(如phone、tablet、2in1、tv、wearable)。
// responsive/env.ts
import display from '@ohos.display';
import deviceInfo from '@ohos.deviceInfo';
export type DeviceKind = 'phone' | 'tablet' | '2in1' | 'tv' | 'wearable' | 'car' | 'unknown'
export function getDeviceKind(): DeviceKind {
const t = (deviceInfo.deviceType || '').toLowerCase()
if (t.includes('phone')) return 'phone'
if (t.includes('tablet')) return 'tablet'
if (t.includes('2in1')) return '2in1'
if (t.includes('tv')) return 'tv'
if (t.includes('wear')) return 'wearable'
if (t.includes('car')) return 'car'
return 'unknown'
}
export function getViewportVp() {
// ArkUI 使用 vp 为布局单位;display 返回 px,记得除以 density
const d = display.getDefaultDisplaySync()
const vpW = d.width / d.densityPixels
const vpH = d.height / d.densityPixels
const landscape = vpW > vpH
return { widthVp: vpW, heightVp: vpH, landscape }
}
注意:不同设备密度差很大,断点最好用 vp,别直接拿 px。
3.2 断点设计(Breakpoints):先定“规则”,再写“代码”
别怕“拍脑袋”,先定个够用的四档断点:
- xs:
< 360vp(极小屏/小窗口)- sm:
360–599vp(常见手机竖屏)- md:
600–1023vp(横屏手机/小平板)- lg:
>= 1024vp(平板/桌面大窗)
// responsive/breakpoints.ts
export type BP = 'xs' | 'sm' | 'md' | 'lg'
export function resolveBP(widthVp: number): BP {
if (widthVp < 360) return 'xs'
if (widthVp < 600) return 'sm'
if (widthVp < 1024) return 'md'
return 'lg'
}
3.3 自适应列数 & 间距策略:Grid/Flex 两个方向都安排上
// responsive/layout.ts
import { resolveBP, BP } from './breakpoints'
import { getViewportVp, getDeviceKind } from './env'
export function gridColumns(): number {
const { widthVp } = getViewportVp()
const bp = resolveBP(widthVp)
const device = getDeviceKind()
// Pad/桌面更激进;穿戴/车载减配
if (device === 'wearable') return 1
if (device === 'tv') return 4
switch (bp) {
case 'xs': return 1
case 'sm': return 2
case 'md': return 3
case 'lg': return device === 'tablet' || device === '2in1' ? 4 : 3
}
}
export function gutters(): number {
const { widthVp } = getViewportVp()
const bp = resolveBP(widthVp)
switch (bp) {
case 'xs': return 6
case 'sm': return 8
case 'md': return 12
case 'lg': return 16
}
}
// Flex 卡片的“最小宽度”参考
export function cardMinWidthVp(): number {
const { widthVp } = getViewportVp()
const bp = resolveBP(widthVp)
switch (bp) {
case 'xs': return 160
case 'sm': return 180
case 'md': return 220
case 'lg': return 260
}
}
3.4 把响应式接到 Grid:模板字符串动态生成
// pages/ResponsiveGrid.ets
import { gridColumns, gutters } from '../responsive/layout'
@Component
struct ResponsiveGrid {
@State cols: number = gridColumns()
@State gap: number = gutters()
aboutToAppear() {
// 监听显示变化(如横竖屏切换/窗口变化),轻量轮询或注册系统回调
// 简化起见,这里用定时刷新策略(工程里可用 display.on('change', ...))
setInterval(() => {
const c = gridColumns()
const g = gutters()
if (c !== this.cols || g !== this.gap) {
this.cols = c; this.gap = g
}
}, 500)
}
private colsTpl(n: number): string {
return new Array(n).fill('1fr').join(' ')
}
build() {
Grid() {
ForEach(new Array(20).fill(0).map((_, i) => i), (i: number) => {
GridItem() {
Column() {
Text(`Tile #${i + 1}`).fontWeight(FontWeight.Medium)
Spacer()
Text('…content…').opacity(0.5)
}
.padding(16)
.height(120)
.borderRadius(12)
.backgroundColor('#F3F4F6')
}
})
}
.columnsTemplate(this.colsTpl(this.cols))
.rowsTemplate('auto')
.columnsGap(this.gap)
.rowsGap(this.gap)
.padding(this.gap)
}
}
效果
- 小屏 1–2 列,转横屏或到 Pad 自动增长列数;
- 间距随断点扩张,信息密度与可读性都照顾。
3.5 把响应式接到 Flex:卡片“最小宽度 + 自适应扩张”
// pages/ResponsiveFlex.ets
import { cardMinWidthVp, gutters } from '../responsive/layout'
@Component
struct ResponsiveFlex {
@State base: number = cardMinWidthVp()
@State gap: number = gutters()
aboutToAppear() {
setInterval(() => {
const b = cardMinWidthVp()
const g = gutters()
if (b !== this.base || g !== this.gap) { this.base = b; this.gap = g }
}, 500)
}
build() {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
ForEach(new Array(30).fill(0).map((_, i) => i), (i: number) => {
Column() {
ProductCard({ title: `Item ${i + 1}`, price: `$ ${(i + 1) * 2}.99` })
}
.width(this.base) // 最小卡片宽度(断点决定)
.flexGrow(1) // 有空间就扩张
.margin({ right: this.gap / 2, left: this.gap / 2, bottom: this.gap })
})
}
.padding(this.gap)
.justifyContent(FlexAlign.Start)
.alignItems(ItemAlign.Stretch)
}
}
理解
width(this.base)给卡片“底线”;.flexGrow(1)允许放大填空;- 整体视觉来自 “最小宽度 + 行内流动”,比 Grid 更“自然”。
3.6 deviceType 适配:不仅是“列数”,是“布局形态”
手机上“单栏滚动”,到了平板就不该还挤成一坨。我们来做一个双栏主从布局:左边列表、右边详情;在手机上自动收敛为单页导航。
// pages/MasterDetail.ets
import { getDeviceKind, getViewportVp } from '../responsive/env'
import { resolveBP } from '../responsive/breakpoints'
@Component
struct MasterDetail {
@State selectedId: number | null = null
private isTwoPane(): boolean {
const { widthVp } = getViewportVp()
const bp = resolveBP(widthVp)
const device = getDeviceKind()
// 平板/大窗 或 2in1 横屏 → 双栏
return bp === 'lg' || device === 'tablet' || device === '2in1'
}
build() {
if (this.isTwoPane()) {
// 双栏
Row() {
// 左:列表
List() {
ForEach(new Array(50).fill(0).map((_, i) => i), (i: number) => ListItem() {
Row() {
Text(`Item ${i + 1}`)
Blank()
if (this.selectedId === i) Text('●').fontColor(Color.Red)
}.onClick(() => this.selectedId = i)
.padding(12)
})
}.width('36%')
// 右:详情
Column() {
if (this.selectedId == null) {
Text('Select an item').opacity(0.5).fontSize(16)
} else {
Text(`Detail of Item ${this.selectedId + 1}`).fontSize(20).fontWeight(FontWeight.Bold)
Text('…long content…').margin({ top: 12 })
}
}.padding(16).layoutWeight(1)
}
.height('100%')
} else {
// 单页:点击跳详情
List() {
ForEach(new Array(50).fill(0).map((_, i) => i), (i: number) => ListItem() {
Row() {
Text(`Item ${i + 1}`)
Blank()
Button('Open').onClick(() => router.pushUrl({ url: 'pages/Detail', params: { id: i } }))
}.padding(12)
})
}
}
}
}
要点
deviceType+bp双重判断,比“只看宽度”更贴近用户心智。- 双栏不是把两个 Column 硬塞在 Row 里那么简单,要给列表和详情合理的宽度比例,常见 30
40% : 6070%。
四、把这三件事揉到一个“可复用”的自适应页
下面是一个集合页:顶部工具条 + 响应式 Grid 主体 + deviceType 驱动的侧边栏(Pad 上显示,手机上隐藏)。拎出去随用随贴。
// pages/AdaptiveDashboard.ets
import { gridColumns, gutters } from '../responsive/layout'
import { getDeviceKind, getViewportVp } from '../responsive/env'
import { resolveBP } from '../responsive/breakpoints'
@Component
struct AdaptiveDashboard {
@State cols: number = gridColumns()
@State gap: number = gutters()
@State showFilters: boolean = true
private isSidePanelVisible(): boolean {
const { widthVp } = getViewportVp()
const bp = resolveBP(widthVp)
const device = getDeviceKind()
return device === 'tablet' || device === '2in1' || bp === 'lg'
}
aboutToAppear() {
setInterval(() => {
const c = gridColumns(), g = gutters()
if (c !== this.cols || g !== this.gap) { this.cols = c; this.gap = g }
}, 500)
}
private colsTpl(n: number) { return new Array(n).fill('1fr').join(' ') }
build() {
Row() {
// 侧栏(Pad/大屏)
if (this.isSidePanelVisible()) {
Column({ space: 12 }) {
Text('Filters').fontWeight(FontWeight.Medium)
TextInput({ placeholder: 'Keyword' })
Button('Apply')
}
.width(240)
.padding(12)
.backgroundColor('#F5F5F5')
}
// 主体
Column({ space: 12 }) {
Row() {
Text('Overview').fontSize(20).fontWeight(FontWeight.Bold)
Blank()
if (!this.isSidePanelVisible()) {
Button(this.showFilters ? 'Hide Filters' : 'Show Filters')
.onClick(() => this.showFilters = !this.showFilters)
}
}.padding({ left: this.gap, right: this.gap, top: 8 })
if (!this.isSidePanelVisible() && this.showFilters) {
Row({ space: 8 }) {
Text('Keyword').width(80)
TextInput({ placeholder: '...' }).layoutWeight(1)
Button('Apply')
}.padding({ left: this.gap, right: this.gap })
}
Grid() {
ForEach(new Array(16).fill(0).map((_, i) => i), (i: number) => {
GridItem() {
Column() {
Text(`Card ${i + 1}`).fontWeight(FontWeight.Medium)
Spacer()
Text('metrics…').opacity(0.5)
}
.padding(16)
.height(120)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 6, color: '#00000022', offsetX: 0, offsetY: 2 })
}
})
}
.columnsTemplate(this.colsTpl(this.cols))
.rowsTemplate('auto')
.columnsGap(this.gap)
.rowsGap(this.gap)
.padding(this.gap)
.layoutWeight(1)
}
.layoutWeight(1)
}
.height('100%')
.backgroundColor('#FAFAFC')
}
}
看点
- 手机上:过滤器以“折叠条”形式出现;
- 平板上:过滤器变成侧栏“常驻”,主体 Grid 自调列数;
- 代码逻辑集中在
isSidePanelVisible / gridColumns / gutters这三处,改断点不改页面。
五、常见坑位 & 处理建议(都是血泪史)
-
Flex 子项宽度同时设置了
width与flexBasis- 建议:选一个主语。多数场景**定
width+flexGrow(1)**就够用。
- 建议:选一个主语。多数场景**定
-
Grid 列数随便写死
- 建议:抽成函数
columnsTemplateFor(n),列数走gridColumns()。
- 建议:抽成函数
-
只看屏幕宽度,忽略 deviceType
- 建议:折叠屏、可穿戴、车机的交互心智完全不同,至少做一下类型分流。
-
横竖屏切换没有刷新 Layout
- 建议:示例里用
setInterval简化;工程中优先监听系统 display/window 变化事件,只在变化时刷新。
- 建议:示例里用
-
Row/Column 深层嵌套过多导致层级爆炸
- 建议:抽组件 + 适时引入 Grid 把“多列”收拾干净;Stack 代替多余的装饰 Row。
-
把“样式变量”写死到处都是
- 建议:把边距/圆角/阴影抽成主题常量,响应式只改一个源。
六、工程落地清单(你可以直接拿这页做项目模板)
-
/responsive
env.ts:deviceType + viewport(vp)breakpoints.ts:断点layout.ts:列数/间距/卡片最小宽度
-
/pages
AdaptiveScaffold.ets:骨架ResponsiveGrid.ets/ResponsiveFlex.ets:两种布局MasterDetail.ets:双栏主从AdaptiveDashboard.ets:侧栏 + 响应式 Grid 一体页
-
/components
ProductCard.ets、MetricCard.ets等卡片
-
/theme
spacing.ts、radius.ts、shadow.ts(按断点导出 token)
七、给未来的自己(和同事)的三句话
- Row/Column 打基础,Flex/Grid 做形变:别用一个容器打天下。
- 断点先定规则,再写代码:可维护性来自“抽象的地方”,不是页面里到处的 if/else。
- deviceType 不是摆设:平板与手机的交互诉求天差地别,敢于在布局形态上“做加法”。
八、附:快速参考手册(贴墙上就行)
Row / Column
Row({ space })、Column({ space }).justifyContent(主轴对齐)、.alignItems(交叉轴对齐).layoutWeight(1):吃满剩余空间
Flex(容器)
.direction(FlexDirection.Row|Column).wrap(FlexWrap.Wrap).justifyContent(FlexAlign.XXX).alignItems(ItemAlign.XXX)
Flex(子项)
.flexGrow(n)、.flexShrink(n)、.flexBasis(len).alignSelf(FlexAlignSelf.XXX)
Grid
.columnsTemplate('1fr 1fr ...').rowsTemplate('auto').columnsGap(n)/.rowsGap(n)GridItem():网格项容器
响应式工具
getViewportVp():拿 vp 的宽高与方向getDeviceKind():phone/tablet/...resolveBP(widthVp):xs/sm/md/lggridColumns()/gutters()/cardMinWidthVp()
结语:自适应不是“加班的借口”,是“下班的资本”
一旦你把 Row/Column → Flex/Grid → 响应式规则 这条链路理顺,ArkUI 的页面就会从“靠堆条件分支”变成“靠规则自动伸缩”。同样的设计稿,在手机上“轻快不拥挤”,在平板上“信息密度恰到好处”,在可折叠设备上“转身不丢脸”。
下次产品说“咱顺手适配下 Pad 吧”,你不需要叹气,只需要微微一笑:“早就适配好了~”
…
(未完待续)
更多推荐


所有评论(0)