11 第10章:多端适配
11.1 10.1 多设备形态概述
11.1.1 10.1.1 HarmonyOS支持的设备类型
HarmonyOS作为分布式操作系统,支持多种设备形态,包括:
- 智能手机:主流设备,屏幕尺寸6.0-7.2英寸,支持触摸交互
- 平板电脑:大屏设备,屏幕尺寸8-12.9英寸,支持手写笔和键盘
- 折叠屏设备:可折叠屏幕,支持展开/折叠两种形态,屏幕尺寸7.6-8英寸(展开状态)
- 智慧屏(电视):超大屏幕,屏幕尺寸55-98英寸,遥控器交互
- 穿戴设备:小屏幕设备,屏幕尺寸1.3-1.9英寸,圆形或方形屏幕
- 车机设备:车载信息娱乐系统,屏幕尺寸8-17英寸,触摸和语音交互
- 智慧屏音箱:中等屏幕,带摄像头和扬声器
11.1.2 10.1.2 设备能力差异分析
不同设备在以下方面存在显著差异:
屏幕尺寸与分辨率
// 设备屏幕信息获取
import device from '@ohos.deviceInfo';
import display from '@ohos.display';
// 获取默认显示信息
let defaultDisplay = display.getDefaultDisplaySync();
console.log(`屏幕宽度: ${defaultDisplay.width}`);
console.log(`屏幕高度: ${defaultDisplay.height}`);
console.log(`屏幕密度: ${defaultDisplay.densityDPI}`);输入方式差异 - 手机/平板:触摸屏、手写笔(平板) - 智慧屏:遥控器、语音 - 穿戴:触摸屏、物理按键、语音 - 车机:触摸屏、语音、方向盘控制
性能约束差异 - 手机/平板:高性能处理器,充足内存 - 穿戴设备:低功耗处理器,有限内存(通常512MB-1GB) - 智慧屏:中等性能,注重图形渲染
11.1.3 10.1.3 自适应设计原则
原则1:内容优先 - 根据屏幕尺寸调整内容展示方式 - 小屏幕:单列布局,突出核心内容 - 大屏幕:多列布局,充分利用空间
原则2:交互自然 - 适配不同输入方式 - 触摸设备:大按钮、手势操作 - 遥控设备:方向键导航、确认键
原则3:性能优化 - 根据设备性能调整资源加载 - 低端设备:简化动画、降低图片质量 - 高端设备:丰富动效、高清资源
原则4:一致性体验 - 保持核心功能和品牌一致性 - 根据设备特点优化交互细节
11.1.4 10.1.4 一次开发多端部署策略
HarmonyOS提供多种策略实现一次开发多端部署:
策略1:响应式布局
// 使用GridRow/GridColumn实现响应式网格
@Entry
@Component
struct ResponsiveGrid {
build() {
GridRow({ columns: 12 }) {
GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
Text('内容1')
.width('100%')
.height(100)
.backgroundColor('#FF6B6B')
}
GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
Text('内容2')
.width('100%')
.height(100)
.backgroundColor('#4ECDC4')
}
GridCol({ span: { xs: 12, sm: 6, md: 4, lg: 3 } }) {
Text('内容3')
.width('100%')
.height(100)
.backgroundColor('#45B7D1')
}
GridCol({ span: { xs: 12, sm: 6, md: 12, lg: 3 } }) {
Text('内容4')
.width('100%')
.height(100)
.backgroundColor('#FFA07A')
}
}
}
}策略2:资源限定词
// 资源目录结构
resources/
├── base/ # 默认资源
│ ├── element/
│ └── media/
├── en_US/ # 英文资源
├── zh_CN/ # 中文资源
├── phone/ # 手机资源
├── tablet/ # 平板资源
├── wearable/ # 穿戴设备资源
├── tv/ # 智慧屏资源
├── car/ # 车机资源
├── foldable/ # 折叠屏资源
└── dark/ # 深色模式资源
策略3:设备类型检测
import device from '@ohos.deviceInfo';
// 检测设备类型
function getDeviceType(): string {
// 通过屏幕尺寸判断
let display = display.getDefaultDisplaySync();
let screenWidth = display.width / display.densityDPI;
if (screenWidth < 3) {
return 'wearable';
} else if (screenWidth < 6) {
return 'phone';
} else if (screenWidth < 9) {
return 'tablet';
} else if (screenWidth < 12) {
return 'foldable';
} else {
return 'tv';
}
}
// 根据设备类型加载不同UI
@Entry
@Component
struct AdaptiveUI {
@State deviceType: string = 'phone';
aboutToAppear() {
this.deviceType = getDeviceType();
}
build() {
if (this.deviceType === 'wearable') {
WearableUI()
} else if (this.deviceType === 'phone') {
PhoneUI()
} else if (this.deviceType === 'tablet') {
TabletUI()
} else {
TabletUI()
}
}
}11.2 10.2 响应式布局
11.2.1 10.2.1 GridRow/GridColumn网格布局
GridRow和GridColumn是HarmonyOS提供的响应式网格布局组件,基于12列网格系统。
基础用法
@Entry
@Component
struct BasicGrid {
build() {
GridRow({ columns: 12, gutter: { x: 16, y: 16 } }) {
GridCol({ span: 6 }) {
Text('左侧内容')
.width('100%')
.height(100)
.backgroundColor('#FF6B6B')
.textAlign(TextAlign.Center)
}
GridCol({ span: 6 }) {
Text('右侧内容')
.width('100%')
.height(100)
.backgroundColor('#4ECDC4')
.textAlign(TextAlign.Center)
}
}
.width('100%')
.padding(16)
}
}响应式断点
@Entry
@Component
struct ResponsiveGrid {
build() {
GridRow({
columns: { sm: 4, md: 8, lg: 12 }, // 不同断点的列数
gutter: { x: 16, y: 16 }
}) {
GridCol({
span: {
sm: 4, // 小屏:占满4列
md: 4, // 中屏:占4列
lg: 3 // 大屏:占3列
}
}) {
Text('内容1')
.width('100%')
.height(100)
.backgroundColor('#FF6B6B')
}
GridCol({
span: {
sm: 4,
md: 4,
lg: 3
}
}) {
Text('内容2')
.width('100%')
.height(100)
.backgroundColor('#4ECDC4')
}
GridCol({
span: {
sm: 4,
md: 8,
lg: 6
}
}) {
Text('内容3')
.width('100%')
.height(100)
.backgroundColor('#45B7D1')
}
}
}
}GridRow参数详解
GridRow({
columns: number | Breakpoints, // 列数或断点配置
gutter: GutterOptions, // 列间距
debug: boolean // 是否显示网格线
})
// GutterOptions
interface GutterOptions {
x?: Length; // 水平间距
y?: Length; // 垂直间距
}
// Breakpoints
interface Breakpoints {
sm?: number; // 小屏列数(默认4)
md?: number; // 中屏列数(默认8)
lg?: number; // 大屏列数(默认12)
}GridCol参数详解
GridCol({
span: number | SpanOptions, // 占用列数
offset: number | OffsetOptions, // 偏移列数
order: number | OrderOptions // 排列顺序
})
// SpanOptions
interface SpanOptions {
sm?: number;
md?: number;
lg?: number;
}
// OffsetOptions
interface OffsetOptions {
sm?: number;
md?: number;
lg?: number;
}
// OrderOptions
interface OrderOptions {
sm?: number;
md?: number;
lg?: number;
}实际应用:卡片列表
@Entry
@Component
struct CardList {
@State cards: string[] = ['卡片1', '卡片2', '卡片3', '卡片4', '卡片5', '卡片6'];
build() {
Scroll() {
GridRow({
columns: { sm: 1, md: 2, lg: 3 },
gutter: { x: 16, y: 16 }
}) {
ForEach(this.cards, (item: string) => {
GridCol({ span: 1 }) {
Column() {
Image($r('app.media.image'))
.width('100%')
.height(150)
.objectFit(ImageFit.Cover)
Text(item)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ top: 12 })
Text('这是卡片描述内容')
.fontSize(14)
.fontColor('#666')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
radius: 8,
color: 'rgba(0,0,0,0.1)',
offsetX: 0,
offsetY: 2
})
}
})
}
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}11.2.2 10.2.2 媒体查询(MediaQuery)
MediaQuery允许根据设备特性(屏幕尺寸、方向、分辨率等)应用不同的样式。
基础用法
import mediaquery from '@ohos.mediaquery';
@Entry
@Component
struct MediaQueryExample {
@State isLargeScreen: boolean = false;
@State isLandscape: boolean = false;
private largeScreenListener?: mediaquery.MediaQueryListener;
private landscapeListener?: mediaquery.MediaQueryListener;
aboutToAppear() {
// 监听大屏(宽度>=600vp)
this.largeScreenListener = mediaquery.matchMediaSync('(width>=600)');
this.largeScreenListener.on('change', (result: mediaquery.MediaQueryResult) => {
this.isLargeScreen = result.matches;
});
// 监听横屏
this.landscapeListener = mediaquery.matchMediaSync('(orientation: landscape)');
this.landscapeListener.on('change', (result: mediaquery.MediaQueryResult) => {
this.isLandscape = result.matches;
});
}
aboutToDisappear() {
this.largeScreenListener?.off('change');
this.landscapeListener?.off('change');
}
build() {
Column() {
if (this.isLargeScreen) {
// 大屏布局
Row() {
Text('左侧导航')
.width(200)
.height('100%')
.backgroundColor('#F0F0F0')
Text('主内容区')
.layoutWeight(1)
.height('100%')
}
} else {
// 小屏布局
Column() {
Text('主内容区')
.width('100%')
.layoutWeight(1)
}
}
}
.width('100%')
.height('100%')
}
}常用媒体查询条件
// 屏幕宽度
'(width>=600)' // 宽度>=600vp
'(width>=600 and width<900)' // 宽度在600-900vp之间
// 屏幕方向
'(orientation: portrait)' // 竖屏
'(orientation: landscape)' // 横屏
// 屏幕密度
'(resolution>=2)' // 像素密度>=2
// 设备类型
'(device-type: phone)' // 手机
'(device-type: tablet)' // 平板
'(device-type: tv)' // 智慧屏
'(device-type: wearable)' // 穿戴设备
'(device-type: car)' // 车机
// 深色模式
'(prefers-color-scheme: dark)' // 深色模式
'(prefers-color-scheme: light)' // 浅色模式综合示例:响应式页面
import mediaquery from '@ohos.mediaquery';
@Entry
@Component
struct ResponsivePage {
@State layout: 'mobile' | 'tablet' | 'desktop' = 'mobile';
private mobileListener?: mediaquery.MediaQueryListener;
private tabletListener?: mediaquery.MediaQueryListener;
aboutToAppear() {
// 移动端(<600vp)
this.mobileListener = mediaquery.matchMediaSync('(width<600)');
this.mobileListener.on('change', (result) => {
if (result.matches) this.layout = 'mobile';
});
// 平板端(600-900vp)
this.tabletListener = mediaquery.matchMediaSync('(width>=600 and width<900)');
this.tabletListener.on('change', (result) => {
if (result.matches) this.layout = 'tablet';
});
// 桌面端(>=900vp)
let desktopListener = mediaquery.matchMediaSync('(width>=900)');
desktopListener.on('change', (result) => {
if (result.matches) this.layout = 'desktop';
});
}
build() {
if (this.layout === 'mobile') {
this.MobileLayout()
} else if (this.layout === 'tablet') {
this.TabletLayout()
} else {
this.DesktopLayout()
}
}
@Builder
MobileLayout() {
Column() {
// 底部导航
Tabs({ barPosition: BarPosition.End }) {
TabContent() { Text('首页') }
TabContent() { Text('发现') }
TabContent() { Text('我的') }
}
}
}
@Builder
TabletLayout() {
Row() {
// 左侧导航
Column() {
Text('首页')
Text('发现')
Text('我的')
}
.width(200)
// 主内容
Column() {
Text('主内容区')
}
.layoutWeight(1)
}
}
@Builder
DesktopLayout() {
Row() {
// 左侧导航
Column() {
Text('Logo')
Text('首页')
Text('发现')
Text('我的')
}
.width(250)
.padding(20)
// 中间内容
Column() {
Text('主内容区')
}
.layoutWeight(1)
.padding(20)
// 右侧边栏
Column() {
Text('推荐')
Text('热门')
}
.width(300)
.padding(20)
}
}
}11.2.3 10.2.3 断点系统设计
断点系统是响应式设计的核心,用于定义不同屏幕尺寸的临界值。
定义断点常量
// Breakpoints.ets
export class Breakpoints {
// 断点值定义
static readonly SM = 0; // 小屏(手机竖屏)
static readonly MD = 600; // 中屏(手机横屏/平板竖屏)
static readonly LG = 840; // 大屏(平板横屏/折叠屏)
static readonly XL = 1200; // 超大屏(桌面)
// 断点名称
static readonly SM_NAME = 'sm';
static readonly MD_NAME = 'md';
static readonly LG_NAME = 'lg';
static readonly XL_NAME = 'xl';
}
// 断点工具类
export class BreakpointSystem {
// 根据宽度获取断点
static getBreakpoint(width: number): string {
if (width < Breakpoints.MD) {
return Breakpoints.SM_NAME;
} else if (width < Breakpoints.LG) {
return Breakpoints.MD_NAME;
} else if (width < Breakpoints.XL) {
return Breakpoints.LG_NAME;
} else {
return Breakpoints.XL_NAME;
}
}
// 获取列数
static getColumns(breakpoint: string): number {
switch (breakpoint) {
case Breakpoints.SM_NAME: return 4;
case Breakpoints.MD_NAME: return 8;
case Breakpoints.LG_NAME: return 12;
case Breakpoints.XL_NAME: return 12;
default: return 12;
}
}
// 获取边距
static getMargin(breakpoint: string): number {
switch (breakpoint) {
case Breakpoints.SM_NAME: return 16;
case Breakpoints.MD_NAME: return 24;
case Breakpoints.LG_NAME: return 32;
case Breakpoints.XL_NAME: return 40;
default: return 16;
}
}
}断点监听器封装
import mediaquery from '@ohos.mediaquery';
import display from '@ohos.display';
@Observed
export class BreakpointState {
current: string = 'sm';
width: number = 0;
columns: number = 4;
margin: number = 16;
}
export class BreakpointListener {
private state: BreakpointState;
private listeners: mediaquery.MediaQueryListener[] = [];
constructor(state: BreakpointState) {
this.state = state;
this.initListeners();
}
private initListeners() {
// 小屏监听
let smListener = mediaquery.matchMediaSync('(width<600)');
smListener.on('change', (result) => {
if (result.matches) {
this.updateBreakpoint('sm');
}
});
this.listeners.push(smListener);
// 中屏监听
let mdListener = mediaquery.matchMediaSync('(width>=600 and width<840)');
mdListener.on('change', (result) => {
if (result.matches) {
this.updateBreakpoint('md');
}
});
this.listeners.push(mdListener);
// 大屏监听
let lgListener = mediaquery.matchMediaSync('(width>=840 and width<1200)');
lgListener.on('change', (result) => {
if (result.matches) {
this.updateBreakpoint('lg');
}
});
this.listeners.push(lgListener);
// 超大屏监听
let xlListener = mediaquery.matchMediaSync('(width>=1200)');
xlListener.on('change', (result) => {
if (result.matches) {
this.updateBreakpoint('xl');
}
});
this.listeners.push(xlListener);
}
private updateBreakpoint(breakpoint: string) {
this.state.current = breakpoint;
this.state.columns = BreakpointSystem.getColumns(breakpoint);
this.state.margin = BreakpointSystem.getMargin(breakpoint);
let displayInfo = display.getDefaultDisplaySync();
this.state.width = displayInfo.width / displayInfo.densityDPI;
}
destroy() {
this.listeners.forEach(listener => {
listener.off('change');
});
}
}使用断点系统
@Entry
@Component
struct BreakpointExample {
@State breakpointState: BreakpointState = new BreakpointState();
private breakpointListener?: BreakpointListener;
aboutToAppear() {
this.breakpointListener = new BreakpointListener(this.breakpointState);
}
aboutToDisappear() {
this.breakpointListener?.destroy();
}
build() {
Column() {
Text(`当前断点: ${this.breakpointState.current}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`列数: ${this.breakpointState.columns}`)
.fontSize(16)
Text(`边距: ${this.breakpointState.margin}`)
.fontSize(16)
// 根据断点显示不同布局
if (this.breakpointState.current === 'sm') {
Text('手机布局')
} else if (this.breakpointState.current === 'md') {
Text('平板布局')
} else {
Text('大屏布局')
}
}
.width('100%')
.padding(this.breakpointState.margin)
}
}11.2.4 10.2.4 弹性布局(Flex)高级用法
Flex布局是响应式设计的重要组成部分,适用于需要灵活调整的场景。
Flex容器属性
Flex({
direction: FlexDirection, // 主轴方向
wrap: FlexWrap, // 换行方式
justifyContent: FlexAlign, // 主轴对齐
alignItems: ItemAlign, // 交叉轴对齐
alignContent: FlexAlign // 多行对齐
})响应式导航栏
@Entry
@Component
struct ResponsiveNav {
@State isExpanded: boolean = true;
build() {
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.NoWrap,
justifyContent: FlexAlign.SpaceBetween,
alignItems: ItemAlign.Center
}) {
// Logo
Text('Logo')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 导航菜单(大屏显示,小屏隐藏)
if (this.isExpanded) {
Flex({
direction: FlexDirection.Row,
gap: 24
}) {
Text('首页')
Text('产品')
Text('关于')
Text('联系')
}
}
// 菜单按钮(小屏显示)
if (!this.isExpanded) {
Button('☰')
.onClick(() => {
// 打开侧边菜单
})
}
}
.width('100%')
.height(60)
.padding({ left: 20, right: 20 })
.backgroundColor(Color.White)
}
}自适应卡片网格
@Entry
@Component
struct AdaptiveCards {
@State items: number[] = Array.from({ length: 20 }, (_, i) => i + 1);
build() {
Scroll() {
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap,
gap: 16
}) {
ForEach(this.items, (item: number) => {
Column() {
Text(`卡片 ${item}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
.width('calc(33.33% - 11px)') // 3列布局
.height(150)
.backgroundColor('#FF6B6B')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
})
}
.padding(16)
}
}
}Flex布局对齐方式
@Entry
@Component
struct FlexAlignExample {
build() {
Column({ space: 30 }) {
// justifyContent示例
Text('justifyContent示例')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Flex({
direction: FlexDirection.Row,
justifyContent: FlexAlign.SpaceBetween,
width: '100%'
}) {
Text('A').width(50).height(50).backgroundColor('#FF6B6B')
Text('B').width(50).height(50).backgroundColor('#4ECDC4')
Text('C').width(50).height(50).backgroundColor('#45B7D1')
}
// alignItems示例
Text('alignItems示例')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Flex({
direction: FlexDirection.Row,
alignItems: ItemAlign.Center,
height: 150,
width: '100%'
}) {
Text('A').width(50).height(50).backgroundColor('#FF6B6B')
Text('B').width(50).height(100).backgroundColor('#4ECDC4')
Text('C').width(50).height(50).backgroundColor('#45B7D1')
}
}
.padding(20)
}
}11.2.5 10.2.5 百分比与相对单位
百分比单位
@Entry
@Component
struct PercentageExample {
build() {
Column() {
// 宽度百分比
Row() {
Text('左侧')
.width('30%')
.height(100)
.backgroundColor('#FF6B6B')
Text('右侧')
.width('70%')
.height(100)
.backgroundColor('#4ECDC4')
}
.width('100%')
// 高度百分比
Row() {
Text('内容')
.width('100%')
.height('50%')
.backgroundColor('#45B7D1')
}
.width('100%')
.height(200)
}
}
}相对单位vp/fp
// vp: 虚拟像素,自动适配屏幕密度
// fp: 字体像素,用于字体大小
@Entry
@Component
struct UnitExample {
build() {
Column() {
Text('文字大小使用fp')
.fontSize(16) // 16fp
Text('大文字')
.fontSize(24) // 24fp
Row() {
Text('宽度使用vp')
.width(100) // 100vp
.height(50) // 50vp
}
}
}
}11.3 10.3 折叠屏适配
11.3.1 10.3.1 折叠/展开状态检测
折叠屏设备可以在折叠和展开状态之间切换,需要实时检测状态变化。
获取折叠屏状态
import foldDisplayMode from '@ohos.foldDisplayMode';
@Entry
@Component
struct FoldableExample {
@State foldStatus: string = 'unknown';
@State foldDisplayMode: foldDisplayMode.FoldDisplayMode =
foldDisplayMode.FoldDisplayMode.FULL;
aboutToAppear() {
// 获取当前折叠状态
this.updateFoldStatus();
// 监听折叠状态变化
foldDisplayMode.on('foldDisplayModeChange', (mode: foldDisplayMode.FoldDisplayMode) => {
this.foldDisplayMode = mode;
this.updateFoldStatus();
});
}
updateFoldStatus() {
let mode = foldDisplayMode.getFoldDisplayMode();
switch (mode) {
case foldDisplayMode.FoldDisplayMode.FULL:
this.foldStatus = '展开状态';
break;
case foldDisplayMode.FoldDisplayMode.HALF:
this.foldStatus = '半折叠状态';
break;
case foldDisplayMode.FoldDisplayMode.FOLDED:
this.foldStatus = '折叠状态';
break;
default:
this.foldStatus = '未知状态';
}
}
aboutToDisappear() {
foldDisplayMode.off('foldDisplayModeChange');
}
build() {
Column() {
Text(`折叠状态: ${this.foldStatus}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
if (this.foldDisplayMode === foldDisplayMode.FoldDisplayMode.FULL) {
this.ExpandedLayout()
} else {
this.FoldedLayout()
}
}
}
@Builder
ExpandedLayout() {
Row() {
// 左侧导航
Column() {
Text('导航1')
Text('导航2')
Text('导航3')
}
.width(300)
.height('100%')
.backgroundColor('#F0F0F0')
// 主内容
Column() {
Text('主内容区')
}
.layoutWeight(1)
.height('100%')
}
}
@Builder
FoldedLayout() {
Column() {
Text('主内容区')
}
.width('100%')
.height('100%')
}
}11.3.2 10.3.2 双屏模式处理
折叠屏在半折叠状态下可以使用双屏模式,上下屏显示不同内容。
双屏布局
import foldDisplayMode from '@ohos.foldDisplayMode';
import display from '@ohos.display';
@Entry
@Component
struct DualScreenExample {
@State isHalfFolded: boolean = false;
aboutToAppear() {
foldDisplayMode.on('foldDisplayModeChange', (mode) => {
this.isHalfFolded = mode === foldDisplayMode.FoldDisplayMode.HALF;
});
}
build() {
if (this.isHalfFolded) {
this.DualScreenLayout()
} else {
this.SingleScreenLayout()
}
}
@Builder
DualScreenLayout() {
Column() {
// 上屏:视频播放
Column() {
Video({
src: $r('app.media.video'),
controller: new VideoController()
})
.width('100%')
.height('100%')
}
.layoutWeight(1)
.backgroundColor(Color.Black)
// 下屏:控制界面
Column() {
Text('视频控制')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
Row({ space: 20 }) {
Button('播放')
Button('暂停')
Button('停止')
}
Slider({
min: 0,
max: 100,
value: 50
})
.margin({ top: 20 })
}
.layoutWeight(1)
.padding(20)
.backgroundColor('#F5F5F5')
}
.width('100%')
.height('100%')
}
@Builder
SingleScreenLayout() {
Column() {
Video({
src: $r('app.media.video'),
controller: new VideoController()
})
.width('100%')
.height(300)
Column() {
Text('视频控制')
// 控制按钮...
}
.layoutWeight(1)
}
}
}11.3.3 10.3.3 屏幕尺寸变化响应
折叠屏展开/折叠时,屏幕尺寸会发生显著变化,需要响应式调整UI。
监听屏幕尺寸变化
import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';
@Entry
@Component
struct ScreenSizeChange {
@State screenWidth: number = 0;
@State screenHeight: number = 0;
private sizeListener?: mediaquery.MediaQueryListener;
aboutToAppear() {
this.updateScreenSize();
// 监听屏幕宽度变化
this.sizeListener = mediaquery.matchMediaSync('(width>=600)');
this.sizeListener.on('change', () => {
this.updateScreenSize();
});
}
updateScreenSize() {
let displayInfo = display.getDefaultDisplaySync();
this.screenWidth = displayInfo.width / displayInfo.densityDPI;
this.screenHeight = displayInfo.height / displayInfo.densityDPI;
}
build() {
Column() {
Text(`屏幕宽度: ${this.screenWidth.toFixed(0)}vp`)
Text(`屏幕高度: ${this.screenHeight.toFixed(0)}vp`)
if (this.screenWidth >= 600) {
// 大屏布局
Row() {
Column() {
Text('左侧')
}
.width('30%')
Column() {
Text('右侧')
}
.layoutWeight(1)
}
} else {
// 小屏布局
Column() {
Text('单列布局')
}
}
}
}
}11.3.4 10.3.4 自适应UI切换
根据折叠状态动态切换UI组件和布局。
自适应内容展示
import foldDisplayMode from '@ohos.foldDisplayMode';
@Entry
@Component
struct AdaptiveFoldableUI {
@State foldMode: foldDisplayMode.FoldDisplayMode =
foldDisplayMode.FoldDisplayMode.FOLDED;
@State articles: Article[] = [];
aboutToAppear() {
foldDisplayMode.on('foldDisplayModeChange', (mode) => {
this.foldMode = mode;
});
}
build() {
if (this.foldMode === foldDisplayMode.FoldDisplayMode.FULL) {
this.ExpandedView()
} else {
this.FoldedView()
}
}
@Builder
ExpandedView() {
Row() {
// 左侧文章列表
List() {
ForEach(this.articles, (article: Article) => {
ListItem() {
ArticleItem({ article: article })
}
})
}
.width(350)
.height('100%')
.backgroundColor('#F9F9F9')
// 右侧文章内容
Column() {
ArticleDetail({ article: this.articles[0] })
}
.layoutWeight(1)
.height('100%')
.padding(20)
}
}
@Builder
FoldedView() {
Navigation() {
List() {
ForEach(this.articles, (article: Article) => {
ListItem() {
ArticleItem({ article: article })
}
})
}
}
.navDestination(this.ArticlePage())
}
@Builder
ArticlePage() {
NavDestination() {
ArticleDetail({ article: this.articles[0] })
}
}
}
interface Article {
id: number;
title: string;
content: string;
}
@Component
struct ArticleItem {
article: Article = { id: 0, title: '', content: '' };
build() {
Column() {
Text(this.article.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
.padding(16)
.width('100%')
}
}
@Component
struct ArticleDetail {
article: Article = { id: 0, title: '', content: '' };
build() {
Column() {
Text(this.article.title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(this.article.content)
.fontSize(16)
.margin({ top: 20 })
}
}
}11.4 10.4 平板适配
11.4.1 10.4.1 大屏布局策略
平板设备屏幕较大,需要充分利用屏幕空间,提供更好的信息展示和交互体验。
多列布局
@Entry
@Component
struct TabletLayout {
@State categories: string[] = ['推荐', '热门', '科技', '娱乐', '体育'];
@State articles: Article[] = [];
build() {
Row() {
// 左侧分类导航
Column() {
Text('分类')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ left: 16, top: 16, bottom: 12 })
ForEach(this.categories, (category: string) => {
Text(category)
.fontSize(16)
.padding({ left: 16, top: 12, bottom: 12, right: 16 })
.width('100%')
.onClick(() => {
// 切换分类
})
})
}
.width(200)
.height('100%')
.backgroundColor('#F5F5F5')
// 中间文章列表
Column() {
List() {
ForEach(this.articles, (article: Article) => {
ListItem() {
ArticleCard({ article: article })
}
})
}
.width('100%')
}
.layoutWeight(1)
.height('100%')
.padding(16)
// 右侧推荐
Column() {
Text('热门推荐')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
ForEach(this.articles.slice(0, 5), (article: Article) => {
Text(article.title)
.fontSize(14)
.padding({ top: 8, bottom: 8 })
})
}
.width(300)
.height('100%')
.padding(16)
.backgroundColor('#FAFAFA')
}
.width('100%')
.height('100%')
}
}
@Component
struct ArticleCard {
article: Article = { id: 0, title: '', content: '' };
build() {
Row() {
Image($r('app.media.article'))
.width(120)
.height(80)
.objectFit(ImageFit.Cover)
.borderRadius(8)
Column() {
Text(this.article.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.article.content)
.fontSize(14)
.fontColor('#666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
}
.layoutWeight(1)
.margin({ left: 12 })
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.1)',
offsetX: 0,
offsetY: 2
})
}
}11.4.3 10.4.3 多窗口支持
平板支持多窗口功能,可以同时显示多个应用或同一应用的多个实例。
多窗口配置
// module.json5
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"supportWindowMode": [
"fullscreen", // 全屏模式
"split", // 分屏模式
"floatingWindow" // 浮动窗口
],
"minWindowWidth": 400, // 最小窗口宽度
"minWindowHeight": 300 // 最小窗口高度
}
]
}
}窗口模式监听
import window from '@ohos.window';
@Entry
@Component
struct MultiWindowExample {
@State windowMode: string = 'fullscreen';
aboutToAppear() {
// 获取当前窗口上下文
let windowClass = window.getLastWindow(getContext(this));
// 监听窗口模式变化
windowClass.on('windowModeChange', (mode: window.WindowMode) => {
switch (mode) {
case window.WindowMode.FULLSCREEN:
this.windowMode = 'fullscreen';
break;
case window.WindowMode.PRIMARY:
this.windowMode = 'split-primary';
break;
case window.WindowMode.SECONDARY:
this.windowMode = 'split-secondary';
break;
case window.WindowMode.FLOATING:
this.windowMode = 'floating';
break;
}
});
}
build() {
Column() {
Text(`当前窗口模式: ${this.windowMode}`)
.fontSize(20)
if (this.windowMode === 'split-primary' || this.windowMode === 'split-secondary') {
// 分屏模式:简化布局
Text('分屏模式')
} else if (this.windowMode === 'floating') {
// 浮动窗口:紧凑布局
Text('浮动窗口')
} else {
// 全屏模式:完整布局
Text('全屏模式')
}
}
}
}11.4.4 10.4.4 手写笔与键盘适配
平板设备支持手写笔和键盘输入,需要适配相应的交互。
手写笔支持
@Entry
@Component
struct PenInputExample {
@State strokes: Stroke[] = [];
@State currentStroke: Stroke = new Stroke();
build() {
Canvas(new CanvasRenderingContext2D(new RenderingContextSettings(true)))
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.currentStroke = new Stroke();
let point = { x: event.touches[0].x, y: event.touches[0].y };
this.currentStroke.points.push(point);
} else if (event.type === TouchType.Move) {
let point = { x: event.touches[0].x, y: event.touches[0].y };
this.currentStroke.points.push(point);
// 实时绘制
this.drawStroke(this.currentStroke);
} else if (event.type === TouchType.Up) {
this.strokes.push(this.currentStroke);
this.currentStroke = new Stroke();
}
})
}
drawStroke(stroke: Stroke) {
// 绘制笔画逻辑
}
}
class Stroke {
points: Point[] = [];
}
interface Point {
x: number;
y: number;
}键盘快捷键
@Entry
@Component
struct KeyboardShortcut {
@State text: string = '';
build() {
Column() {
TextArea({ text: this.text, placeholder: '输入文本...' })
.width('100%')
.height(200)
.onKeyEvent((event: KeyEvent) => {
// Ctrl+S 保存
if (event.ctrlKey && event.key === 's') {
this.saveDocument();
event.stopPropagation();
}
// Ctrl+Z 撤销
if (event.ctrlKey && event.key === 'z') {
this.undo();
event.stopPropagation();
}
// Ctrl+Y 重做
if (event.ctrlKey && event.key === 'y') {
this.redo();
event.stopPropagation();
}
})
}
}
saveDocument() {
console.log('保存文档');
}
undo() {
console.log('撤销');
}
redo() {
console.log('重做');
}
}11.5 10.5 穿戴设备适配
11.5.1 10.5.1 小屏幕UI设计
穿戴设备屏幕较小(通常1.3-1.9英寸),需要精简UI元素,突出核心功能。
紧凑布局
@Entry
@Component
struct WearableUI {
@State steps: number = 8562;
@State heartRate: number = 72;
@State calories: number = 320;
build() {
Column() {
// 步数显示
Column() {
Text('步数')
.fontSize(12)
.fontColor('#999')
Text(this.steps.toString())
.fontSize(36)
.fontWeight(FontWeight.Bold)
.margin({ top: 4 })
}
.margin({ top: 20 })
// 心率和卡路里
Row({ space: 20 }) {
Column() {
Text('心率')
.fontSize(10)
.fontColor('#999')
Text(`${this.heartRate} bpm`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 2 })
}
Column() {
Text('卡路里')
.fontSize(10)
.fontColor('#999')
Text(`${this.calories} kcal`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 2 })
}
}
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Black)
.fontColor(Color.White)
}
}11.5.2 10.5.2 圆形屏幕适配
部分穿戴设备采用圆形屏幕,需要适配圆形显示区域。
圆形屏幕布局
@Entry
@Component
struct CircularScreen {
build() {
Stack() {
// 圆形背景
Circle()
.width('100%')
.height('100%')
.fill('#1A1A1A')
// 内容区域(限制在圆形范围内)
Column() {
Text('12:30')
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('周一, 3月15日')
.fontSize(14)
.fontColor('#CCC')
.margin({ top: 8 })
Row({ space: 16 }) {
Text('❤️ 72')
.fontSize(12)
.fontColor('#FF6B6B')
Text('🔥 320')
.fontSize(12)
.fontColor('#FFA500')
}
.margin({ top: 16 })
}
.width('70%') // 限制内容宽度
.height('70%') // 限制内容高度
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
}
}11.5.3 10.5.3 精简交互模式
穿戴设备交互方式有限,需要简化交互流程。
手势交互
@Entry
@Component
struct WearableGesture {
@State currentPage: number = 0;
build() {
Swiper() {
// 页面1:运动数据
Column() {
Text('今日运动')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('8562步')
.fontSize(32)
.margin({ top: 12 })
}
// 页面2:心率
Column() {
Text('心率')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('72 bpm')
.fontSize(32)
.margin({ top: 12 })
}
// 页面3:卡路里
Column() {
Text('卡路里')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('320 kcal')
.fontSize(32)
.margin({ top: 12 })
}
}
.index(this.currentPage)
.indicator(false) // 隐藏指示器
.loop(false)
.onChange((index: number) => {
this.currentPage = index;
})
.width('100%')
.height('100%')
}
}11.5.4 10.5.4 性能约束下的优化
穿戴设备性能有限,需要优化资源使用。
性能优化策略
@Entry
@Component
struct WearableOptimization {
@State dataList: number[] = [];
aboutToAppear() {
// 延迟加载数据
setTimeout(() => {
this.dataList = [1, 2, 3, 4, 5];
}, 100);
}
build() {
List() {
ForEach(this.dataList, (item: number) => {
ListItem() {
// 简化UI组件
Text(`项目 ${item}`)
.fontSize(14)
.padding(12)
}
})
}
.width('100%')
.cachedCount(2) // 限制缓存数量
.scrollBar(BarState.Off) // 关闭滚动条
}
}资源优化
// 使用小尺寸图片
Image($r('app.media.icon_small')) // 而不是 icon_large
.width(48)
.height(48)
// 避免复杂动画
Text('数据')
.fontSize(16)
// 不使用 .animation() 或减少动画时长
// 减少重绘
@Component
struct OptimizedComponent {
@Prop data: string; // 使用@Prop而不是@Link,减少更新
build() {
Text(this.data)
}
}11.6 10.6 多端适配最佳实践
11.6.1 10.6.1 资源限定词(Resource Qualifier)
资源限定词允许为不同设备配置提供不同的资源文件。
资源目录结构
resources/
├── base/ # 默认资源
│ ├── element/
│ │ ├── color.json
│ │ └── string.json
│ └── media/
│ └── icon.png
├── phone/ # 手机资源
│ └── media/
│ └── icon_phone.png
├── tablet/ # 平板资源
│ └── media/
│ └── icon_tablet.png
├── wearable/ # 穿戴设备资源
│ └── media/
│ └── icon_wearable.png
├── tv/ # 智慧屏资源
│ └── media/
│ └── icon_tv.png
├── foldable/ # 折叠屏资源
│ └── media/
│ └── icon_foldable.png
├── car/ # 车机资源
│ └── media/
│ └── icon_car.png
├── dark/ # 深色模式
│ └── element/
│ └── color.json
├── en_US/ # 英文
│ └── element/
│ └── string.json
└── zh_CN/ # 中文
└── element/
└── string.json
使用资源限定词
@Entry
@Component
struct ResourceQualifier {
build() {
Column() {
// 自动根据设备类型加载对应图片
Image($r('app.media.icon'))
.width(100)
.height(100)
// 自动根据语言加载字符串
Text($r('app.string.hello'))
.fontSize(20)
// 自动根据深色/浅色模式加载颜色
Text('文本')
.fontColor($r('app.color.text_primary'))
}
}
}颜色资源定义
// base/element/color.json
{
"color": [
{
"name": "text_primary",
"value": "#000000"
},
{
"name": "background",
"value": "#FFFFFF"
}
]
}
// dark/element/color.json
{
"color": [
{
"name": "text_primary",
"value": "#FFFFFF"
},
{
"name": "background",
"value": "#121212"
}
]
}11.6.2 10.6.2 动态资源加载
根据设备类型动态加载不同资源。
动态加载图片
import device from '@ohos.deviceInfo';
@Entry
@Component
struct DynamicResource {
@State deviceType: string = 'phone';
aboutToAppear() {
this.deviceType = this.getDeviceType();
}
getDeviceType(): string {
// 根据设备特征判断类型
let displayInfo = display.getDefaultDisplaySync();
let screenWidth = displayInfo.width / displayInfo.densityDPI;
if (screenWidth < 3) return 'wearable';
if (screenWidth < 6) return 'phone';
if (screenWidth < 9) return 'tablet';
return 'tv';
}
build() {
Column() {
if (this.deviceType === 'wearable') {
Image($r('app.media.icon_small'))
.width(48)
.height(48)
} else if (this.deviceType === 'phone') {
Image($r('app.media.icon_medium'))
.width(96)
.height(96)
} else {
Image($r('app.media.icon_large'))
.width(192)
.height(192)
}
}
}
}11.6.3 10.6.3 设备类型检测
准确检测设备类型是适配的基础。
综合设备检测
import device from '@ohos.deviceInfo';
import display from '@ohos.display';
import mediaquery from '@ohos.mediaquery';
export class DeviceDetector {
// 检测设备类型
static getDeviceType(): 'phone' | 'tablet' | 'foldable' | 'tv' | 'wearable' | 'car' {
let displayInfo = display.getDefaultDisplaySync();
let screenWidthInch = displayInfo.width / displayInfo.densityDPI;
let screenHeightInch = displayInfo.height / displayInfo.densityDPI;
let diagonalInch = Math.sqrt(screenWidthInch * screenWidthInch + screenHeightInch * screenHeightInch);
if (diagonalInch < 2) {
return 'wearable';
} else if (diagonalInch < 7) {
return 'phone';
} else if (diagonalInch < 11) {
// 可能是平板或折叠屏
return this.isFoldable() ? 'foldable' : 'tablet';
} else if (diagonalInch < 20) {
return 'car';
} else {
return 'tv';
}
}
// 检测是否为折叠屏
static isFoldable(): boolean {
try {
// 检查是否支持折叠屏API
let foldMode = foldDisplayMode.getFoldDisplayMode();
return foldMode !== undefined;
} catch (e) {
return false;
}
}
// 检测屏幕方向
static isLandscape(): boolean {
let displayInfo = display.getDefaultDisplaySync();
return displayInfo.width > displayInfo.height;
}
// 检测是否为深色模式
static isDarkMode(): boolean {
// 通过资源限定词或系统API检测
return false;
}
// 获取屏幕尺寸信息
static getScreenInfo() {
let displayInfo = display.getDefaultDisplaySync();
return {
width: displayInfo.width,
height: displayInfo.height,
density: displayInfo.densityDPI,
widthInch: displayInfo.width / displayInfo.densityDPI,
heightInch: displayInfo.height / displayInfo.densityDPI
};
}
}11.6.4 10.6.4 自适应组件封装
封装可复用的自适应组件。
自适应按钮
@Component
export struct AdaptiveButton {
text: string = '';
onClick: () => void = () => {};
@Prop deviceType: string = 'phone';
build() {
Button(this.text)
.fontSize(this.getFontSize())
.height(this.getHeight())
.padding(this.getPadding())
.onClick(this.onClick)
}
getFontSize(): number {
switch (this.deviceType) {
case 'wearable': return 12;
case 'phone': return 14;
case 'tablet': return 16;
case 'tv': return 20;
default: return 14;
}
}
getHeight(): number {
switch (this.deviceType) {
case 'wearable': return 32;
case 'phone': return 40;
case 'tablet': return 48;
case 'tv': return 56;
default: return 40;
}
}
getPadding(): Padding {
switch (this.deviceType) {
case 'wearable': return { left: 12, right: 12 };
case 'phone': return { left: 16, right: 16 };
case 'tablet': return { left: 24, right: 24 };
case 'tv': return { left: 32, right: 32 };
default: return { left: 16, right: 16 };
}
}
}自适应列表
@Component
export struct AdaptiveList<T> {
items: T[] = [];
@BuilderParam itemBuilder: (item: T) => void;
@Prop deviceType: string = 'phone';
build() {
if (this.deviceType === 'wearable') {
// 穿戴设备:简单列表
List() {
ForEach(this.items, (item: T) => {
ListItem() {
this.itemBuilder(item)
}
})
}
.scrollBar(BarState.Off)
} else if (this.deviceType === 'tv') {
// 智慧屏:网格布局
GridRow({ columns: 4 }) {
ForEach(this.items, (item: T) => {
GridCol({ span: 1 }) {
this.itemBuilder(item)
}
})
}
} else {
// 手机/平板:标准列表
List() {
ForEach(this.items, (item: T) => {
ListItem() {
this.itemBuilder(item)
}
})
}
.divider({ strokeWidth: 1, color: '#F0F0F0' })
}
}
}11.6.5 10.6.5 多端测试策略
测试矩阵
// 定义测试设备矩阵
export const TestDevices = {
phone: {
name: 'Phone',
screen: { width: 1080, height: 2340, density: 480 },
orientation: ['portrait', 'landscape']
},
tablet: {
name: 'Tablet',
screen: { width: 2560, height: 1600, density: 320 },
orientation: ['portrait', 'landscape']
},
foldable: {
name: 'Foldable',
screen: {
folded: { width: 1080, height: 2340, density: 480 },
unfolded: { width: 2200, height: 2340, density: 480 }
},
orientation: ['portrait', 'landscape']
},
wearable: {
name: 'Wearable',
screen: { width: 466, height: 466, density: 320 },
shape: 'circular'
},
tv: {
name: 'TV',
screen: { width: 3840, height: 2160, density: 320 },
orientation: 'landscape'
}
};自动化测试脚本
import { describe, it, expect } from '@ohos/hypium';
export default function multiDeviceTest() {
describe('MultiDeviceTest', () => {
// 测试不同设备类型的布局
it('test_phone_layout', 0, () => {
// 模拟手机设备
let deviceType = 'phone';
let layout = getAdaptiveLayout(deviceType);
expect(layout.type).assertEqual('single_column');
});
it('test_tablet_layout', 0, () => {
// 模拟平板设备
let deviceType = 'tablet';
let layout = getAdaptiveLayout(deviceType);
expect(layout.type).assertEqual('multi_column');
});
it('test_wearable_layout', 0, () => {
// 模拟穿戴设备
let deviceType = 'wearable';
let layout = getAdaptiveLayout(deviceType);
expect(layout.type).assertEqual('compact');
});
});
}
function getAdaptiveLayout(deviceType: string) {
// 根据设备类型返回布局配置
if (deviceType === 'phone') {
return { type: 'single_column', columns: 1 };
} else if (deviceType === 'tablet') {
return { type: 'multi_column', columns: 3 };
} else if (deviceType === 'wearable') {
return { type: 'compact', columns: 1 };
}
return { type: 'default', columns: 1 };
}11.7 本章小结
本章深入讲解了HarmonyOS多端适配的核心技术和最佳实践:
多设备形态:了解了HarmonyOS支持的设备类型及其能力差异,掌握了自适应设计原则和一次开发多端部署策略。
响应式布局:深入学习了GridRow/GridColumn网格布局、媒体查询、断点系统设计、Flex弹性布局以及百分比与相对单位的使用。
折叠屏适配:掌握了折叠/展开状态检测、双屏模式处理、屏幕尺寸变化响应和自适应UI切换技术。
平板适配:学习了大屏布局策略、分栏导航(NavigationSplitView)、多窗口支持以及手写笔与键盘适配。
穿戴设备适配:了解了小屏幕UI设计、圆形屏幕适配、精简交互模式和性能约束下的优化方法。
多端适配最佳实践:掌握了资源限定词、动态资源加载、设备类型检测、自适应组件封装和多端测试策略。
通过本章学习,开发者能够构建真正意义上”一次开发,多端部署”的HarmonyOS应用,为不同设备形态的用户提供最佳体验。
11.8 练习题
题目1(单选题):HarmonyOS中,用于实现响应式网格布局的组件是?
A. Flex和Column B. GridRow和GridColumn C. Row和Column D. Stack和RelativeContainer
题目2(单选题):在HarmonyOS多端适配中,断点系统的主要作用是?
A. 提高应用性能 B. 根据屏幕尺寸应用不同布局 C. 管理应用生命周期 D. 处理用户输入
题目3(多选题):以下哪些是HarmonyOS支持的设备类型?
A. 智能手机 B. 平板电脑 C. 智慧屏 D. 穿戴设备
题目4(判断题):资源限定词只能用于区分不同语言,不能用于区分不同设备类型。
题目5(单选题):折叠屏设备在半折叠状态下,可以使用哪种模式实现上下屏显示不同内容?
A. 全屏模式 B. 分屏模式 C. 双屏模式 D. 浮动窗口模式
题目6(判断题):平板适配中,NavigationSplitView组件最多支持两栏导航。
题目7(单选题):穿戴设备适配中,为了优化性能,应该采取以下哪种策略?
A. 使用高清大图 B. 增加复杂动画 C. 限制列表缓存数量 D. 启用所有系统功能
题目8(多选题):以下哪些是HarmonyOS多端适配的最佳实践?
A. 使用资源限定词 B. 实现响应式布局 C. 检测设备类型 D. 为每个设备开发独立应用