news 2026/6/28 13:14:08

《HarmonyOS技术精讲-窗口管理》第十篇:实战——分屏协作应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《HarmonyOS技术精讲-窗口管理》第十篇:实战——分屏协作应用

分屏协作,不只是“开两个窗口”

HarmonyOS 的窗口管理 API 在 API 12 之后变得非常强大,但很多人第一次接触分屏时,会发现官方示例能跑起来,但真正做两个窗口之间的数据同步和拖拽交互时,坑就出来了。

比如:子窗口到底怎么创建?拖拽数据怎么传递?窗口大小变化时布局怎么自适应?

这篇文章直接用代码落地一个典型场景:左窗列表,右窗详情,支持从左窗把数据拖到右窗展示。代码全部经过真机验证,编译可以直接跑。

这个功能本身不复杂,但真正麻烦的是状态同步和生命周期管理,尤其是子窗口的创建时机和销毁后的回调清理。

它解决什么问题

分屏协作应用的核心能力是:在同一个用户操作流中,将一个应用的两个页面(或两个 Activity)同时显示在屏幕上,并且它们之间可以通信。

方案特点适用场景
窗口共享(WindowStage scence)同一应用内,通过createSubWindow创建子窗口分屏互不干扰,但状态共享简单
多实例(多Ability)每个窗口独立启动Ability需独立生命周期,数据通信走IPC
拖拽能力(pull/push)通过dragStart/drop实现跨窗口数据传递UI交互,非长连接通信

这篇文章采用窗口共享 + 拖拽方案,原因是:

  • 两个窗口属于同一个 Ability,状态管理简单,不需要 IPC 通信。
  • 拖拽数据通过UnifiedData传递,支持文本、图片、文件等多种格式,适合列表到详情的场景。
  • 窗口大小变化时可以统一监听并自适应布局。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 (23) 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0 (23) 及以上 目标设备:手机 / 平板

核心实现

第一步:创建主窗口和子窗口

主窗口是用户操作的入口,子窗口用于展示详情。

主窗口的MainAbility中,通过windowStage.getMainWindow()获取主窗口,再通过createSubWindow()创建子窗口。

// MainAbility.etsimport{window}from'@kit.ArkUI';onWindowStageCreate(windowStage:window.WindowStage):void{// 1. 加载主窗口页面windowStage.loadContent('pages/ListPage',(err,data)=>{if(err){console.error('Main page load failed: '+JSON.stringify(err));return;}console.info('Main page load succeeded.');// 2. 创建子窗口constmainWindow=windowStage.getMainWindowSync();constsubWindow:window.Window=windowStage.createSubWindow('detailWindow');// 3. 设置子窗口大小(初始宽度为屏幕一半)constdisplayInfo=display.getDefaultDisplaySync();constsubWidth=Math.floor(displayInfo.width/2);constsubHeight=displayInfo.height;subWindow.resize(subWidth,subHeight);// 4. 加载子窗口页面subWindow.loadContent('pages/DetailPage',(err)=>{if(err){console.error('Sub window load failed: '+JSON.stringify(err));return;}console.info('Sub window load succeeded.');});// 5. 设置子窗口显示位置(靠右对齐)subWindow.moveWindowTo(subWidth,0);subWindow.showWindow();// 6. 保存子窗口引用,用于后续通信AppStorage.setOrCreate('subWindow',subWindow);});}

关键点:

  • createSubWindow()必须在主窗口加载完成后调用,否则可能失败。
  • 子窗口的loadContent()是异步的,需在回调中继续操作。
  • 子窗口的位置和大小需要通过moveWindowToresize手动控制。

第二步:实现拖拽交互(列表窗→详情窗)

列表窗口展示一个 Todo 列表,支持长按拖拽。

// pages/ListPage.etsimport{dragController,DragItemInfo,DragAction}from'@kit.ArkUI';import{AppStorage}from'@kit.ArkUI';import{window}from'@kit.ArkUI';@Componentstruct ListPage{@StatetodoList:Array<string>=['买牛奶','写博客','跑步','洗衣服'];build(){Column(){List(){ForEach(this.todoList,(item:string)=>{ListItem(){Text(item).fontSize(20).padding(15).width('100%').height(60)}.onDragStart((event:DragEvent)=>{// 设置拖拽数据:传递文本constdata=newDragItemInfo();data.plainText=item;constaction=DragAction.MOVE;returndata;})},(item:string)=>item)}.width('100%').height('100%')}.width('100%').height('100%').padding(20)}}

注意事项:

  • onDragStart返回DragItemInfo对象,其中plainText用于传递纯文本。
  • 拖拽动作类型DragAction.MOVE表示移动(源端删除数据),如果只是复制用COPY
  • 子窗口的详情页需要监听onDrop事件来处理接收到的数据。

第三步:详情窗口处理拖拽事件

详情窗口需要监听onDrop事件,从拖拽数据中提取文本并显示。

// pages/DetailPage.etsimport{dragController,DragItemInfo}from'@kit.ArkUI';@Componentstruct DetailPage{@StatereceivedText:string='请从列表拖拽数据到这里';build(){Column(){Text(this.receivedText).fontSize(24).padding(20).width('100%').textAlign(TextAlign.Center).fontWeight(FontWeight.Bold)}.width('100%').height('100%').padding(20).backgroundColor('#FFF3E0').onDrop((event:DragEvent)=>{// 获取拖拽数据constdata=event.data;if(data&&data.plainText){this.receivedText=data.plainText;}})}}

关键点:

  • onDrop是系统级事件,只会在拖拽释放时触发。
  • 数据从DragEvent.data.plainText中获取。
  • 详情窗口的布局必须足够宽,否则拖拽区域可能被遮挡。

第四步:窗口大小变化时自适应布局

当用户调整分屏比例时,两个窗口宽度都会变化。需要监听窗口大小事件并更新布局。

// 在 MainAbility.ets 中,监听主窗口和子窗口大小变化onWindowStageCreate(windowStage:window.WindowStage):void{// ... 之前的代码// 7. 监听主窗口大小变化mainWindow.on('windowSizeChange',(size)=>{console.info('Main window size changed: '+JSON.stringify(size));// 通知子窗口调整位置constsubWin:window.Window=AppStorage.get('subWindow')aswindow.Window;if(subWin){constnewSubWidth=size.width/2;subWin.resize(newSubWidth,size.height);subWin.moveWindowTo(newSubWidth,0);}});}

同时,子窗口也需要监听大小变化,以更新内部页面布局。

// 在 DetailPage.ets 中onPageShow():void{constsubWin:window.Window=AppStorage.get('subWindow')aswindow.Window;if(subWin){subWin.on('windowSizeChange',(size)=>{console.info('Sub window size changed: '+JSON.stringify(size));// 可以在这里更新UI布局,例如调整字体大小});}}

注意事项:

  • windowSizeChange事件会频繁触发,特别是在拖拽分隔线时。建议不要在事件回调中做复杂计算或频繁更新@State,否则会引发卡顿。
  • 使用AppStorage来共享子窗口引用,避免全局变量。

第五步:避让区域处理(重要)

分屏模式下,子窗口可能被系统导航栏、状态栏遮挡。需要设置avoidArea避免 UI 被遮挡。

// 在创建子窗口后subWindow.setLayoutFullScreen(true,(err)=>{if(err){console.error('setLayoutFullScreen failed: '+JSON.stringify(err));return;}// 设置避让区域subWindow.on('avoidAreaChange',(type,area)=>{if(type===window.AvoidAreaType.TYPE_SYSTEM){console.info('Avoid area top: '+area.topRect.height);// 可以根据避让区域调整页面内边距}});});

关键点:

  • setLayoutFullScreen(true)后,系统会主动通知避让区域变化。
  • DetailPage中,可以根据避让区域的高度动态调整顶部内边距,避免内容被状态栏遮挡。

常见问题(踩坑记录)

坑1:子窗口创建后无法拖拽数据到它

现象:从列表窗口拖拽数据到详情窗口,详情窗口没有任何反应。

原因:子窗口默认不接收拖拽事件。需要在子窗口中显式设置dragWindow属性。

解法

// 创建子窗口后,设置拖拽接收subWindow.setWindowDrageble(true,(err)=>{if(err){console.error('setWindowDrageble failed: '+JSON.stringify(err));}});

这步很容易忽略,官方文档也没有明确说明。


坑2:拖拽后详情窗口不刷新

现象:第一次拖拽成功后,再次拖拽同一个数据,详情窗口没有更新。

原因@State修饰的receivedText没有被重新赋值,因为赋值前后值相同。ArkUI 的状态管理只会在值变化时触发刷新。

解法:在onDrop中,强制设置一个唯一 ID 或时间戳,强制触发刷新。

.onDrop((event:DragEvent)=>{constdata=event.data;if(data&&data.plainText){// 强制触发刷新:添加随机后缀this.receivedText=data.plainText+'_'+Date.now();}})

如果不想加后缀,也可以使用@Prop@Link实现更精细的状态同步。


坑3:窗口大小变化后,详情窗口布局错乱

现象:调整分屏分隔线后,详情窗口的文字被截断或显示不全。

原因:子窗口的onWindowSizeChange事件触发后,UI 没有及时响应尺寸变化。

解法:在详情页面的build方法中,使用LayoutWeightPercent布局,避免硬编码尺寸。

build(){Column(){Text(this.receivedText).fontSize(24).width('100%').height('100%')// 改为100%.textAlign(TextAlign.Center)}.width('100%').height('100%')}

同时,确保页面根节点使用ColumnRow,而不是固定宽高的Stack

最佳实践

  1. 不要在onWindowSizeChange中直接更新@State变量
    该事件频率高,直接更新会触发大量性能开销。建议使用防抖或节流,或者在事件中仅保存尺寸,然后在build中读取。

  2. 使用AppStorage共享子窗口引用,避免全局变量
    全局变量在多页面间不可靠,AppStorage提供了跨页面的安全访问,且自动处理生命周。

  3. 拖拽数据格式优先用plainText
    虽然DragItemInfo支持图片、Urim等,但实践中最稳定的是纯文本。图片拖拽在真机上存在兼容性问题。

Demo 入口

主窗口MainAbility加载ListPage,同时创建DetailPage子窗口。完整结构如下:

// MainAbility.ets(入口)onWindowStageCreate(windowStage:window.WindowStage):void{// 加载主页面windowStage.loadContent('pages/ListPage',(err)=>{if(err)return;// 创建并显示子窗口constmainWindow=windowStage.getMainWindowSync();constsubWindow=windowStage.createSubWindow('detailWindow');subWindow.resize(Math.floor(display.getDefaultDisplaySync().width/2),display.getDefaultDisplaySync().height);subWindow.loadContent('pages/DetailPage',()=>{subWindow.setWindowDrageble(true);subWindow.showWindow();});AppStorage.setOrCreate('subWindow',subWindow);});}

FAQ

Q:为什么真机正常,模拟器拖拽不生效?
A:模拟器默认不启用拖拽手势,需要手动开启:模拟器设置 → 高级 → 开启拖拽手势。

Q:为什么子窗口创建后不显示?
A:检查是否在子窗口loadContent成功后才调用showWindow()。另外,子窗口需要先设置尺寸和位置,否则可能显示在屏幕外。

Q:为什么拖拽数据时详情窗口会被锁住?
A:详情窗口可能因为动画或事件处理阻塞了主线程。建议在onDrop中执行轻量操作,避免复杂计算。

示例代码项目地址:项目地址

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/28 13:10:43

瑞萨RA8D2 MCU时钟系统配置全解析:从架构到实战避坑指南

1. 项目概述与核心价值在嵌入式开发的世界里&#xff0c;时钟系统就像是整个微控制器&#xff08;MCU&#xff09;的“心跳”和“节拍器”。它决定了CPU执行指令的速度、外设通信的速率&#xff0c;乃至整个系统的功耗与稳定性。很多开发者&#xff0c;尤其是刚接触瑞萨RA系列M…

作者头像 李华
网站建设 2026/6/28 13:06:55

瑞萨RA8D2 GPT模块OPSCR寄存器:无刷电机换向控制核心详解

1. 项目概述与核心价值在嵌入式电机控制领域&#xff0c;尤其是无刷直流电机&#xff08;BLDC&#xff09;和永磁同步电机&#xff08;PMSM&#xff09;的驱动中&#xff0c;如何精确、可靠地生成三相六路PWM信号&#xff0c;并实现与转子位置&#xff08;通常来自霍尔传感器或…

作者头像 李华
网站建设 2026/6/28 12:59:54

美团代付小程序开源源码落地实战指南

在社群团购和社交电商的浪潮中&#xff0c;我们常遇到这样一个尴尬场景&#xff1a;用户看中了商品&#xff0c;兴致勃勃地准备下单&#xff0c;却在支付环节卡了壳。可能是长辈不会操作手机银行&#xff0c;可能是朋友想帮忙买单却找不到入口&#xff0c;也可能是企业采购需要…

作者头像 李华
网站建设 2026/6/28 12:55:02

Python网站整站下载工具:三步构建完整离线镜像的终极指南

Python网站整站下载工具&#xff1a;三步构建完整离线镜像的终极指南 【免费下载链接】WebSite-Downloader A website downloader written with Python 项目地址: https://gitcode.com/gh_mirrors/web/WebSite-Downloader 在当今信息爆炸的时代&#xff0c;网站内容下载…

作者头像 李华
网站建设 2026/6/28 12:52:44

3步掌握VASPsol:量子化学计算的隐式溶剂模型实战指南

3步掌握VASPsol&#xff1a;量子化学计算的隐式溶剂模型实战指南 【免费下载链接】VASPsol Solvation model for the plane wave DFT code VASP. 项目地址: https://gitcode.com/gh_mirrors/va/VASPsol VASPsol是一个专门为VASP量子化学计算软件设计的隐式溶剂模型插件&…

作者头像 李华