HarmonyOS APP分屏:从模式启动到数据共享,打造丝滑的分屏体验

2026-08-24 15:47:10 | 世界杯的规则

HarmonyOS APP分屏:从模式启动到数据共享,打造丝滑的分屏体验

📌 核心要点:分屏不是简单的"屏幕一分为二",而是布局适配、数据共享、生命周期协同、用户体验设计的综合考验

一、背景与动机

想象一下:你正在看一份长文档,同时需要在备忘录里做笔记。来回切换应用?效率太低了。这时候分屏就派上用场了——左边看文档,右边写笔记,两不耽误。

在折叠屏、平板等大屏设备上,分屏几乎是"刚需"。HarmonyOS 从系统层面提供了完善的分屏支持,用户可以通过手势或任务管理器轻松进入分屏模式。但对于开发者来说,分屏带来的挑战可不少:

• 你的页面在半屏宽度下还能正常显示吗?

• 分屏的两个窗口之间需要共享数据吗?

• 分屏切换时,生命周期怎么处理?

• 怎么设计才能让分屏体验真正好用?

这篇文章,咱们就把分屏开发这件事讲透。

二、核心原理

2.1 分屏模式的工作机制

2.2 分屏下的窗口生命周期

分屏模式下,两个窗口各自有独立的生命周期。关键点在于:两个窗口可能属于同一个应用,也可能属于不同应用。

2.3 分屏适配的核心策略

分屏适配的核心是响应式布局。HarmonyOS 提供了多种布局适配手段:

适配手段

说明

适用场景

媒体查询

根据窗口宽度切换布局

简单的布局切换

栅格布局

自适应列数

列表、卡片布局

弹性布局

按比例分配空间

通用布局

自定义断点

精细控制布局变化

复杂页面

三、代码实战

3.1 分屏模式检测与响应式布局

这是检测分屏状态并根据窗口宽度切换布局的完整示例:

// SplitScreenAdapter.etsimport { window } from '@kit.ArkUI';import { common } from '@kit.AbilityKit';// 布局断点定义const BREAKPOINT_SM = 360; // 小屏(分屏窄侧)const BREAKPOINT_MD = 600; // 中屏(分屏宽侧)const BREAKPOINT_LG = 840; // 大屏(全屏)// 布局模式枚举enum LayoutMode { COMPACT = 'compact', // 紧凑模式:单列 MEDIUM = 'medium', // 中等模式:双列 EXPANDED = 'expanded' // 展开模式:三列+侧边栏}@Entry@Componentstruct SplitScreenAdapterPage { // 当前布局模式 @State layoutMode: LayoutMode = LayoutMode.EXPANDED; // 当前窗口宽度 @State windowWidth: number = 0; // 是否处于分屏模式 @State isSplitScreen: boolean = false; // 示例数据 @State articles: string[] = [ 'HarmonyOS 窗口管理详解', 'ArkUI 响应式布局实战', '分屏开发最佳实践', '多窗口通信机制', '折叠屏适配指南', '应用性能优化技巧' ]; // 窗口监听回调ID private windowSizeChangeCallback?: (size: window.Size) => void; aboutToAppear(): void { // 获取当前窗口并注册大小变化监听 const context = getContext(this) as common.UIAbilityContext; try { const win = window.findWindow(context.abilityInfo.name); // 初始计算布局 const props = win.getWindowProperties(); this.updateLayoutMode(props.windowRect.width); // 监听窗口大小变化(分屏进入/退出时触发) this.windowSizeChangeCallback = (size: window.Size) => { console.info(`[SplitScreen] 窗口大小变化: ${size.width}x${size.height}`); this.windowWidth = size.width; this.updateLayoutMode(size.width); }; win.on('windowSizeChange', this.windowSizeChangeCallback); } catch (e) { console.error('[SplitScreen] 获取窗口失败'); } } aboutToDisappear(): void { // 移除监听 if (this.windowSizeChangeCallback) { const context = getContext(this) as common.UIAbilityContext; try { const win = window.findWindow(context.abilityInfo.name); win.off('windowSizeChange'); } catch (e) { // 忽略 } } } /** * 根据窗口宽度更新布局模式 */ private updateLayoutMode(width: number): void { this.windowWidth = width; if (width < BREAKPOINT_SM) { this.layoutMode = LayoutMode.COMPACT; this.isSplitScreen = true; } else if (width < BREAKPOINT_MD) { this.layoutMode = LayoutMode.MEDIUM; this.isSplitScreen = true; } else { this.layoutMode = LayoutMode.EXPANDED; this.isSplitScreen = false; } console.info(`[SplitScreen] 布局模式: ${this.layoutMode}, 分屏: ${this.isSplitScreen}`); } build() { // 根据布局模式切换不同的UI结构 if (this.layoutMode === LayoutMode.COMPACT) { this.buildCompactLayout(); } else if (this.layoutMode === LayoutMode.MEDIUM) { this.buildMediumLayout(); } else { this.buildExpandedLayout(); } } /** * 紧凑布局(分屏窄侧) * 单列显示,简化内容 */ @Builder buildCompactLayout() { Column({ space: 8 }) { // 简化的标题栏 Row() { Text('📰 文章') .fontSize(16) .fontWeight(FontWeight.Bold) } .width('100%') .padding({ left: 12, right: 12, top: 8, bottom: 8 }) // 单列文章列表 List({ space: 8 }) { ForEach(this.articles, (article: string, index: number) => { ListItem() { Text(article) .fontSize(13) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .padding(10) .width('100%') .borderRadius(8) .backgroundColor('#1a1a2e') } }, (article: string, index: number) => `${index}`) } .width('100%') .layoutWeight(1) .padding({ left: 8, right: 8 }) } .width('100%') .height('100%') .backgroundColor('#0d0d1a') .foregroundColor('#fff') } /** * 中等布局(分屏宽侧) * 双列显示 */ @Builder buildMediumLayout() { Column({ space: 8 }) { // 标题栏 Row() { Text('📰 文章列表') .fontSize(18) .fontWeight(FontWeight.Bold) Blank() if (this.isSplitScreen) { Text('分屏模式') .fontSize(11) .fontColor('#4CAF50') .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .borderRadius(4) .backgroundColor('#4CAF5022') } } .width('100%') .padding(12) // 双列网格 GridRow({ columns: 2, gutter: 8 }) { ForEach(this.articles, (article: string, index: number) => { GridCol() { Column({ space: 4 }) { Text(article) .fontSize(14) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(`作者 ${index + 1}`) .fontSize(11) .fontColor('#888') } .padding(12) .borderRadius(10) .backgroundColor('#1a1a2e') } }, (article: string, index: number) => `${index}`) } .width('100%') .padding({ left: 8, right: 8 }) } .width('100%') .height('100%') .backgroundColor('#0d0d1a') .foregroundColor('#fff') } /** * 展开布局(全屏) * 侧边栏 + 三列内容 */ @Builder buildExpandedLayout() { Row() { // 侧边导航栏 Column({ space: 16 }) { Text('📰') .fontSize(24) Text('📁') .fontSize(24) Text('⚙️') .fontSize(24) } .width(60) .height('100%') .padding({ top: 40 }) .backgroundColor('#111122') .justifyContent(FlexAlign.Start) // 主内容区 Column({ space: 8 }) { Row() { Text('📰 文章列表') .fontSize(20) .fontWeight(FontWeight.Bold) Blank() Text('全屏模式') .fontSize(11) .fontColor('#2196F3') .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .borderRadius(4) .backgroundColor('#2196F322') } .width('100%') .padding(12) // 三列网格 GridRow({ columns: 3, gutter: 12 }) { ForEach(this.articles, (article: string, index: number) => { GridCol() { Column({ space: 8 }) { Text(article) .fontSize(15) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(`作者 ${index + 1} · 3分钟阅读`) .fontSize(12) .fontColor('#888') } .padding(16) .borderRadius(12) .backgroundColor('#1a1a2e') } }, (article: string, index: number) => `${index}`) } .width('100%') .padding({ left: 12, right: 12 }) } .layoutWeight(1) .height('100%') .backgroundColor('#0d0d1a') .foregroundColor('#fff') } .width('100%') .height('100%') }}

3.2 分屏间数据共享

分屏场景下,两个窗口之间经常需要共享数据。比如左边看文章,右边做笔记——笔记需要引用文章中的内容。这个示例展示分屏间的数据共享:

// SplitScreenDataShare.etsimport { emitter } from '@kit.BasicServicesKit';import { window } from '@kit.ArkUI';import { common } from '@kit.AbilityKit';import { relationalStore } from '@kit.ArkData';// 数据共享事件const EVENT_ARTICLE_SELECT = 30001; // 文章选中const EVENT_NOTE_UPDATE = 30002; // 笔记更新// 文章数据模型interface Article { id: number; title: string; content: string;}// ===== 文章阅读页面(分屏左侧) =====@Entry@Componentstruct ArticleReaderPage { @State currentArticle: Article = { id: 1, title: 'HarmonyOS 分屏开发详解', content: '分屏模式是HarmonyOS的重要特性之一,它允许用户同时使用两个应用...' }; @State articles: Article[] = [ { id: 1, title: 'HarmonyOS 分屏开发详解', content: '分屏模式是HarmonyOS的重要特性之一...' }, { id: 2, title: '响应式布局最佳实践', content: '响应式布局是适配不同屏幕尺寸的关键技术...' }, { id: 3, title: '窗口间通信机制', content: '在多窗口场景下,窗口间通信是核心能力...' }, ]; @State selectedText: string = ''; build() { Column({ space: 12 }) { // 文章标题 Text(this.currentArticle.title) .fontSize(20) .fontWeight(FontWeight.Bold) .padding(12) // 文章列表 List({ space: 8 }) { ForEach(this.articles, (article: Article) => { ListItem() { Text(article.title) .fontSize(14) .padding(10) .width('100%') .borderRadius(8) .backgroundColor(article.id === this.currentArticle.id ? '#4CAF5033' : '#1a1a2e') .onClick(() => { this.currentArticle = article; // 通知笔记窗口文章切换 emitter.emit({ eventId: EVENT_ARTICLE_SELECT }, { data: { articleId: article.id, title: article.title } }); }) } }, (article: Article) => `${article.id}`) } .width('100%') .height(80) // 文章内容(可选中文字) Scroll() { Text(this.currentArticle.content) .fontSize(15) .lineHeight(24) .copyOption(CopyOption.LocalDevice) // 允许复制 .onAreaChange(() => {}, () => {}) } .layoutWeight(1) .padding(12) // 发送选中文字到笔记窗口 Row() { TextInput({ placeholder: '输入引用文字...' }) .layoutWeight(1) .onChange((value: string) => { this.selectedText = value; }) Button('引用') .onClick(() => { if (this.selectedText) { emitter.emit({ eventId: EVENT_NOTE_UPDATE }, { data: { action: 'quote', text: this.selectedText, source: this.currentArticle.title } }); this.selectedText = ''; } }) } .width('100%') .padding(12) } .width('100%') .height('100%') .backgroundColor('#0d0d1a') .foregroundColor('#fff') }}// ===== 笔记编辑页面(分屏右侧) =====@Entry@Componentstruct NoteEditorPage { @State noteContent: string = ''; @State currentArticleTitle: string = ''; @State quotes: string[] = []; aboutToAppear(): void { // 监听文章选中事件 emitter.on({ eventId: EVENT_ARTICLE_SELECT }, (eventData) => { this.currentArticleTitle = eventData.data?.title as string; console.info(`[NoteEditor] 切换到文章: ${this.currentArticleTitle}`); }); // 监听引用事件 emitter.on({ eventId: EVENT_NOTE_UPDATE }, (eventData) => { const action = eventData.data?.action as string; if (action === 'quote') { const text = eventData.data?.text as string; const source = eventData.data?.source as string; this.quotes.push(`📌 来自《${source}》: ${text}`); console.info(`[NoteEditor] 收到引用: ${text}`); } }); } aboutToDisappear(): void { emitter.off(EVENT_ARTICLE_SELECT); emitter.off(EVENT_NOTE_UPDATE); } build() { Column({ space: 12 }) { // 标题 Row() { Text('📝 笔记') .fontSize(18) .fontWeight(FontWeight.Bold) if (this.currentArticleTitle) { Text(`正在阅读: ${this.currentArticleTitle}`) .fontSize(11) .fontColor('#4CAF50') .margin({ left: 8 }) } } .width('100%') .padding(12) // 引用列表 if (this.quotes.length > 0) { List({ space: 6 }) { ForEach(this.quotes, (quote: string, index: number) => { ListItem() { Text(quote) .fontSize(12) .fontColor('#aaa') .padding(8) .width('100%') .borderRadius(6) .backgroundColor('#4CAF5011') } }, (quote: string, index: number) => `${index}`) } .width('100%') .height(80) .padding({ left: 12, right: 12 }) } // 笔记编辑区 TextArea({ placeholder: '在这里记录笔记...', text: this.noteContent }) .width('92%') .layoutWeight(1) .onChange((value: string) => { this.noteContent = value; }) // 保存按钮 Button('保存笔记') .width('80%') .backgroundColor('#4CAF50') .onClick(() => this.saveNote()) } .width('100%') .height('100%') .backgroundColor('#0d0d1a') .foregroundColor('#fff') } /** * 保存笔记到本地数据库 */ private saveNote(): void { // 实际项目中使用 relationalStore 持久化 console.info(`[NoteEditor] 笔记已保存: ${this.noteContent.substring(0, 50)}...`); // 通知文章窗口笔记已保存 emitter.emit({ eventId: EVENT_NOTE_UPDATE }, { data: { action: 'saved', noteLength: this.noteContent.length } }); }}

3.3 分屏适配的完整 Ability 管理

这个示例展示在 Ability 层面如何管理分屏的完整生命周期:

// SplitScreenAbility.etsimport { UIAbility, AbilityConstant, WindowStage } from '@kit.AbilityKit';import { window } from '@kit.ArkUI';import { BusinessError } from '@kit.BasicServicesKit';export default class SplitScreenAbility extends UIAbility { private mainWindow: window.Window | null = null; // 当前是否处于分屏模式 private isInSplitScreen: boolean = false; // 分屏前的窗口大小 private preSplitSize: window.Size | null = null; onWindowStageCreate(windowStage: WindowStage): void { console.info('[SplitAbility] onWindowStageCreate'); windowStage.getMainWindow().then((win: window.Window) => { this.mainWindow = win; // 设置全屏布局 win.setWindowLayoutFullScreen(true); // 监听窗口大小变化(分屏的核心监听) win.on('windowSizeChange', (size: window.Size) => { this.handleWindowSizeChange(size); }); // 监听避让区域变化 win.on('avoidAreaChange', (data: window.AvoidAreaInfo) => { this.handleAvoidAreaChange(data); }); // 监听窗口状态变化 win.on('windowStatusChange', (status: window.WindowStatus) => { this.handleWindowStatusChange(status); }); }); windowStage.loadContent('pages/SplitScreenAdapter'); } /** * 处理窗口大小变化 * 这是分屏适配的核心入口 */ private handleWindowSizeChange(size: window.Size): void { const display = window.getDefaultDisplaySync(); const screenWidth = display.width; // 判断是否进入分屏模式 // 如果窗口宽度明显小于屏幕宽度,说明进入了分屏 const wasInSplitScreen = this.isInSplitScreen; this.isInSplitScreen = size.width < screenWidth * 0.7; if (this.isInSplitScreen && !wasInSplitScreen) { // 从全屏进入分屏 console.info('[SplitAbility] 进入分屏模式'); this.preSplitSize = { width: size.width, height: size.height } as window.Size; this.onEnterSplitScreen(size); } else if (!this.isInSplitScreen && wasInSplitScreen) { // 从分屏退出到全屏 console.info('[SplitAbility] 退出分屏模式'); this.onExitSplitScreen(size); } else if (this.isInSplitScreen) { // 分屏中的窗口大小调整 console.info(`[SplitAbility] 分屏中窗口调整: ${size.width}x${size.height}`); this.onSplitScreenResize(size); } // 通知UI层当前窗口尺寸 AppStorage.setOrCreate('windowWidth', size.width); AppStorage.setOrCreate('windowHeight', size.height); AppStorage.setOrCreate('isSplitScreen', this.isInSplitScreen); } /** * 进入分屏模式时的处理 */ private onEnterSplitScreen(size: window.Size): void { // 1. 降低动画复杂度(分屏时减少GPU压力) AppStorage.setOrCreate('reduceAnimation', true); // 2. 切换到简化布局 AppStorage.setOrCreate('layoutMode', 'compact'); // 3. 暂停非必要后台任务 // this.pauseNonCriticalTasks(); console.info('[SplitAbility] 分屏模式适配完成'); } /** * 退出分屏模式时的处理 */ private onExitSplitScreen(size: window.Size): void { // 1. 恢复动画复杂度 AppStorage.setOrCreate('reduceAnimation', false); // 2. 恢复完整布局 AppStorage.setOrCreate('layoutMode', 'expanded'); // 3. 恢复后台任务 // this.resumeNonCriticalTasks(); this.preSplitSize = null; console.info('[SplitAbility] 退出分屏模式,恢复全屏'); } /** * 分屏中窗口大小调整 */ private onSplitScreenResize(size: window.Size): void { // 根据新的窗口大小调整布局 if (size.width < 360) { AppStorage.setOrCreate('layoutMode', 'compact'); } else if (size.width < 600) { AppStorage.setOrCreate('layoutMode', 'medium'); } } /** * 处理避让区域变化 */ private handleAvoidAreaChange(data: window.AvoidAreaInfo): void { if (data.type === window.AvoidAreaType.TYPE_SYSTEM) { // 状态栏高度变化(分屏时可能不同) const statusBarHeight = data.area.topRect.height; AppStorage.setOrCreate('statusBarHeight', statusBarHeight); } } /** * 处理窗口状态变化 */ private handleWindowStatusChange(status: window.WindowStatus): void { if (status === window.WindowStatus.FOCUSED) { // 窗口获焦:恢复动画、刷新数据 AppStorage.setOrCreate('reduceAnimation', false); } else { // 窗口失焦:降低动画、暂停刷新 AppStorage.setOrCreate('reduceAnimation', true); } } onWindowStageDestroy(): void { if (this.mainWindow) { this.mainWindow.off('windowSizeChange'); this.mainWindow.off('avoidAreaChange'); this.mainWindow.off('windowStatusChange'); } this.mainWindow = null; }}

四、踩坑与注意事项

4.1 分屏不是"缩放"

坑:分屏后页面只是等比缩放,内容变小了但布局没变。

分屏适配的核心是布局重构,不是缩放。一个 360dp 宽的分屏窗口应该显示单列布局,而不是把三列布局压缩到 360dp 里。

// ❌ 错误:用缩放适配分屏// 整个页面变小,文字看不清// ✅ 正确:用布局重构适配分屏// 根据窗口宽度切换不同的布局结构if (width < 360) { // 单列紧凑布局} else if (width < 600) { // 双列中等布局} else { // 三列展开布局}

4.2 分屏下的触摸区域

坑:分屏后按钮太小,触摸困难。

分屏模式下窗口变窄,原本的按钮和触摸目标可能变得太小。建议:

• 最小触摸目标不小于 44×44 vp

• 适当增大分屏模式下的按钮尺寸

• 减少并排按钮的数量

4.3 分屏下的图片加载

坑:分屏后仍然加载全屏尺寸的图片,浪费内存和带宽。

// ❌ 错误:分屏下加载全屏尺寸图片Image('https://example.com/image_1920x1080.jpg')// ✅ 正确:根据窗口宽度加载合适尺寸的图片Image(this.getImageUrlByWidth(windowWidth))private getImageUrlByWidth(width: number): string { if (width < 360) { return 'https://example.com/image_360x240.jpg'; } else if (width < 600) { return 'https://example.com/image_600x400.jpg'; } else { return 'https://example.com/image_1920x1080.jpg'; }}

4.4 分屏与键盘冲突

坑:分屏模式下弹出键盘,窗口空间更加局促。

分屏+键盘是"双重压缩",需要特别处理:

• 监听 avoidAreaChange,键盘弹出时调整布局

• 考虑使用 scroll 包裹内容,确保关键信息可见

• 键盘弹出时隐藏不必要的装饰性元素

4.5 分屏下的生命周期陷阱

坑:分屏切换时没有正确处理数据保存。

分屏切换可能导致窗口重建(比如从分屏退出时),如果不保存状态,用户数据会丢失:

// ✅ 正确:分屏切换时保存状态onWindowStageDestroy(): void { // 保存当前页面状态 AppStorage.setOrCreate('savedScrollPosition', this.scrollPosition); AppStorage.setOrCreate('savedSelectedIndex', this.selectedIndex);}

五、HarmonyOS 6 适配

5.1 API 变化

特性

HarmonyOS 5.0

HarmonyOS 6.0

分屏模式检测

通过 windowSizeChange 推断

新增 isSplitScreenMode() API

分屏比例

固定 50:50

支持自定义比例(30:70 等)

分屏间拖拽

不支持

新增跨窗口拖拽能力

分屏恢复

手动保存状态

新增状态自动保存/恢复

三分屏

不支持

部分设备支持三分屏

5.2 迁移要点

1. 分屏检测:HarmonyOS 6 新增 isSplitScreenMode() API,不再需要通过窗口宽度推断

2. 分屏比例:支持更灵活的分屏比例,布局适配需要覆盖更多宽度值

3. 跨窗口拖拽:新增的拖拽能力可以让用户在分屏间直接拖拽内容

4. 三分屏:部分大屏设备支持三分屏,布局适配需要考虑更窄的窗口

5.3 兼容性写法

// 兼容 HarmonyOS 5.0 和 6.0 的分屏检测private checkSplitScreen(win: window.Window): boolean { // HarmonyOS 6.0:使用新 API // if (typeof win.isSplitScreenMode === 'function') { // return win.isSplitScreenMode(); // } // HarmonyOS 5.0:通过窗口宽度推断 const display = window.getDefaultDisplaySync(); const props = win.getWindowProperties(); return props.windowRect.width < display.width * 0.7;}

六、总结

知识点

核心内容

关键API

分屏启动

手势/API触发,系统管理

windowSizeChange 监听

分屏检测

窗口宽度推断分屏状态

getWindowProperties

响应式布局

根据宽度切换布局模式

断点判断 + 条件渲染

数据共享

Emitter/AppStorage/数据库

emitter, AppStorage

分屏适配

布局重构、触摸区域、图片加载

@Builder 条件渲染

生命周期

进入/退出分屏的状态管理

onEnterSplitScreen

性能优化

降低动画、按需加载资源

AppStorage 状态控制

用户体验

简化布局、增大触摸目标

设计规范

一句话总结:分屏适配的核心是"布局跟着窗口走"——窗口变小就切换紧凑布局,窗口变大就展开丰富布局。记住三个关键词:响应式布局(适配宽度)、数据共享(窗口间通信)、性能优化(分屏时减负),分屏开发就稳了。