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

发布时间:2026/6/28 13:14:36
《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}fromkit.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. 创建子窗口constmainWindowwindowStage.getMainWindowSync();constsubWindow:window.WindowwindowStage.createSubWindow(detailWindow);// 3. 设置子窗口大小初始宽度为屏幕一半constdisplayInfodisplay.getDefaultDisplaySync();constsubWidthMath.floor(displayInfo.width/2);constsubHeightdisplayInfo.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()是异步的需在回调中继续操作。子窗口的位置和大小需要通过moveWindowTo和resize手动控制。第二步实现拖拽交互列表窗→详情窗列表窗口展示一个 Todo 列表支持长按拖拽。// pages/ListPage.etsimport{dragController,DragItemInfo,DragAction}fromkit.ArkUI;import{AppStorage}fromkit.ArkUI;import{window}fromkit.ArkUI;Componentstruct ListPage{StatetodoList:Arraystring[买牛奶,写博客,跑步,洗衣服];build(){Column(){List(){ForEach(this.todoList,(item:string){ListItem(){Text(item).fontSize(20).padding(15).width(100%).height(60)}.onDragStart((event:DragEvent){// 设置拖拽数据传递文本constdatanewDragItemInfo();data.plainTextitem;constactionDragAction.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}fromkit.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){// 获取拖拽数据constdataevent.data;if(datadata.plainText){this.receivedTextdata.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.WindowAppStorage.get(subWindow)aswindow.Window;if(subWin){constnewSubWidthsize.width/2;subWin.resize(newSubWidth,size.height);subWin.moveWindowTo(newSubWidth,0);}});}同时子窗口也需要监听大小变化以更新内部页面布局。// 在 DetailPage.ets 中onPageShow():void{constsubWin:window.WindowAppStorage.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(typewindow.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){constdataevent.data;if(datadata.plainText){// 强制触发刷新添加随机后缀this.receivedTextdata.plainText_Date.now();}})如果不想加后缀也可以使用Prop或Link实现更精细的状态同步。坑3窗口大小变化后详情窗口布局错乱现象调整分屏分隔线后详情窗口的文字被截断或显示不全。原因子窗口的onWindowSizeChange事件触发后UI 没有及时响应尺寸变化。解法在详情页面的build方法中使用LayoutWeight或Percent布局避免硬编码尺寸。build(){Column(){Text(this.receivedText).fontSize(24).width(100%).height(100%)// 改为100%.textAlign(TextAlign.Center)}.width(100%).height(100%)}同时确保页面根节点使用Column或Row而不是固定宽高的Stack。最佳实践不要在onWindowSizeChange中直接更新State变量该事件频率高直接更新会触发大量性能开销。建议使用防抖或节流或者在事件中仅保存尺寸然后在build中读取。使用AppStorage共享子窗口引用避免全局变量全局变量在多页面间不可靠AppStorage提供了跨页面的安全访问且自动处理生命周。拖拽数据格式优先用plainText虽然DragItemInfo支持图片、Urim等但实践中最稳定的是纯文本。图片拖拽在真机上存在兼容性问题。Demo 入口主窗口MainAbility加载ListPage同时创建DetailPage子窗口。完整结构如下// MainAbility.ets入口onWindowStageCreate(windowStage:window.WindowStage):void{// 加载主页面windowStage.loadContent(pages/ListPage,(err){if(err)return;// 创建并显示子窗口constmainWindowwindowStage.getMainWindowSync();constsubWindowwindowStage.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);});}FAQQ为什么真机正常模拟器拖拽不生效A模拟器默认不启用拖拽手势需要手动开启模拟器设置 → 高级 → 开启拖拽手势。Q为什么子窗口创建后不显示A检查是否在子窗口loadContent成功后才调用showWindow()。另外子窗口需要先设置尺寸和位置否则可能显示在屏幕外。Q为什么拖拽数据时详情窗口会被锁住A详情窗口可能因为动画或事件处理阻塞了主线程。建议在onDrop中执行轻量操作避免复杂计算。示例代码项目地址项目地址