【HarmonyOS Bug踩坑】主窗口调用的UI表现在子窗口异常显示
一、问题现象:
这个问题的标题略显抽象,毕竟涉及到的异常表现形式太多,标题是临时拟定的。
说白了,这个问题是鸿蒙里经典的上下文指定问题。
异常的业务场景是,在主窗口之上,添加一个子窗口。当在主窗口里调用某些UI表现,例如:气泡,弹窗,模态窗口,自定义安全键盘,自定义loading等,你会发现,有时候都异常加载到子窗口中了,并没有在主窗口显示。如下图所示:
1import { window } from '@kit.ArkUI'; 2import { BusinessError } from '@kit.BasicServicesKit'; 3 4@Entry 5@Component 6struct SubWinPage { 7 private TAG: string = "SubWinPage"; 8 private sub_windowClass: window.Window | null = null; 9 10 aboutToAppear() { 11 this.showSubWindow("xxxx", 900, 300) 12 setTimeout(()=>{ 13 try { 14 this.destroySubWindow(); 15 // window.getLastWindow(getContext()).then((win)=>{ 16 // console.error(this.TAG, 'win:' + JSON.stringify(win)); 17 // let height = win.getWindowDecorHeight(); 18 // console.error(this.TAG, 'height:' + height); 19 // }) 20 21 let windowStage_: window.WindowStage = globalThis.mWindowStage; 22 let win = windowStage_.getMainWindowSync(); 23 let height = win.getWindowDecorHeight(); 24 }catch (e){ 25 console.error(this.TAG, 'e:' + JSON.stringify(e)); 26 } 27 },1000) 28 } 29 30 private showSubWindow(name: string, num: number, x: number) { 31 console.log(this.TAG, 'showSubWindow start'); 32 let windowStage_: window.WindowStage = globalThis.mWindowStage; 33 // 1.创建应用子窗口。 34 if (windowStage_ == null) { 35 console.error(this.TAG, 'Failed to create the subwindow. Cause: windowStage_ is null'); 36 } 37 else { 38 windowStage_.createSubWindow(name, (err: BusinessError, data) => { 39 let errCode: number = err.code; 40 if (errCode) { 41 console.error(this.TAG, 'Failed to create the subwindow. Cause: ' + JSON.stringify(err)); 42 return; 43 } 44 this.sub_windowClass = data; 45 console.info(this.TAG, 'Succeeded in creating the subwindow. Data: ' + JSON.stringify(data)); 46 // 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。 47 this.sub_windowClass.moveWindowTo(x, 300, (err: BusinessError) => { 48 let errCode: number = err.code; 49 if (errCode) { 50 console.error(this.TAG, 'Failed to move the window. Cause:' + JSON.stringify(err)); 51 return; 52 } 53 console.info(this.TAG, 'Succeeded in moving the window.'); 54 }); 55 this.sub_windowClass.resize(num, num, (err: BusinessError) => { 56 let errCode: number = err.code; 57 if (errCode) { 58 console.error(this.TAG, 'Failed to change the window size. Cause:' + JSON.stringify(err)); 59 return; 60 } 61 console.info(this.TAG, 'Succeeded in changing the window size.'); 62 }); 63 // 3.为子窗口加载对应的目标页面。 64 this.sub_windowClass.setUIContent("pages/SubWinLoadPage", (err: BusinessError) => { 65 let errCode: number = err.code; 66 if (errCode) { 67 console.error(this.TAG, 'Failed to load the content. Cause:' + JSON.stringify(err)); 68 return; 69 } 70 this.sub_windowClass?.setWindowTouchable(true) 71 console.info(this.TAG, 'Succeeded in loading the content.'); 72 // 3.显示子窗口。 (this.sub_windowClass as window.Window) 73 this.sub_windowClass?.showWindow((err: BusinessError) => { 74 let errCode: number = err.code; 75 if (errCode) { 76 console.error(this.TAG, 'Failed to show the window. Cause: ' + JSON.stringify(err)); 77 return; 78 } 79 console.info(this.TAG, 'Succeeded in showing the window.'); 80 }); 81 }); 82 }) 83 } 84 console.log(this.TAG, 'showSubWindow end'); 85 } 86 87 destroySubWindow() { 88 // 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。 89 (this.sub_windowClass as window.Window).destroyWindow((err: BusinessError) => { 90 let errCode: number = err.code; 91 if (errCode) { 92 console.error(this.TAG, 'Failed to destroy the window. Cause: ' + JSON.stringify(err)); 93 return; 94 } 95 console.info(this.TAG, 'Succeeded in destroying the window.'); 96 }); 97 } 98 99 build() { 100 Column() { 101 Text("点击创建子窗口") 102 .id('SubWinPageHelloWorld') 103 .fontSize(50) 104 .fontWeight(FontWeight.Bold) 105 .onClick(()=>{ 106 this.showSubWindow("ooooo", 500, 300); 107 }) 108 109 Text("点击创建子窗口2") 110 .id('SubWinPageHelloWorld') 111 .fontSize(50) 112 .fontWeight(FontWeight.Bold) 113 .onClick(()=>{ 114 this.showSubWindow("ooooo2", 800, 600); 115 }) 116 117 Text("移动窗口2") 118 .id('SubWinPageHelloWorld') 119 .fontSize(50) 120 .fontWeight(FontWeight.Bold) 121 .onClick(()=>{ 122 123 this.sub_windowClass?.moveWindowTo(700, 300, (err: BusinessError) => { 124 let errCode: number = err.code; 125 if (errCode) { 126 console.error(this.TAG, 'Failed to move the window. Cause:' + JSON.stringify(err)); 127 return; 128 } 129 console.info(this.TAG, 'Succeeded in moving the window.'); 130 }); 131 }) 132 133 Text("点击销毁子窗口") 134 .id('SubWinPageHelloWorld') 135 .fontSize(50) 136 .fontWeight(FontWeight.Bold) 137 .onClick(()=>{ 138 this.destroySubWindow(); 139 }) 140 141 Text("显示气泡") 142 .id('SubWinPageHelloWorld') 143 .fontSize(50) 144 .fontWeight(FontWeight.Bold) 145 .onClick(async ()=>{ 146 let win: window.Window = await window.getLastWindow(getContext()); 147 win.getUIContext().getPromptAction().showToast({ 148 message: "我是气泡,测试显示问题", 149 duration: 5000 150 }); 151 152 win.getUIContext().px2vp(200) 153 }) 154 } 155 .height('100%') 156 .width('100%') 157 .justifyContent(FlexAlign.Center) 158 } 159} 160
甚至还有使用老路由router跳转,在主窗口跳转也有可能会加载到子窗口中。或者一些使用逻辑处理的业务也可能异常。
该问题多出现在较大的工程里,使用了Har包,Hsp包。或者使用MVVM架构,纯逻辑层处理业务等。
二、问题原由:
综上所述,说了这么多,其实问题的原因在开头已经提到了,罪魁祸首就是上下文。Context。
如果接触鸿蒙开发,时间比较长的同学,其实对上下文还是很熟悉的,你会发现在日常开发中,经常要用到上下文去调用某些接口。特别是从api7最早开始接触鸿蒙,到如今api20了。这样的老同学体会更深,会发现很多之前不需要使用上下文调用的接口,现在也推荐或者强制让使用上下文引入接口了。
例如气泡,px2vp等等。
1getUIContext().getPromptAction().showToast({ 2 message: "我是气泡,测试显示问题", 3 duration: 5000 4 }); 5 6getUIContext().px2vp(200) 7
其实该问题就是因为上下文依赖,因为鸿蒙特殊的堆叠渲染树,需要通过上下文作为挂靠节点的标志位。有了上下文作为导航,就知道当前要渲染的UI控件,现在挂载到哪个节点下了。
最早的时候,上下文是沉入到系统底层,上层开发感知比较少,很少需要调用到上下文去引出接口。但是随着接入鸿蒙的app越来越多。很多复杂的项目和业务场景迁移到鸿蒙中,发现这样的设计方式有问题。很多UI挂载的预期很离谱。
所以随着API的升级,上下文慢慢开放到应用层,让开发者来灵活的使用,来掌控挂载的预期效果。
该问题大多出现在api12或者之前的老项目中,因为封装的逻辑层,需要用到上下文。多是通过getLastWindow的形式,获取窗口,再从窗口中拿上下文。来做UI的引用操作。但是当有子窗口显示时,getLastWindow其实拿就不是主窗口,而是子窗口,这也导致后续获取的上下文也是子窗口的。
1 let win: window.Window = await window.getLastWindow(getContext()); 2 win.getUIContext().getPromptAction().showToast({ 3 message: "我是气泡,测试显示问题", 4 duration: 5000 5 }); 6
像Loading,弹窗,甚至是悬浮活动按钮,都喜欢用子窗口来做,都会导致该问题。虽然子窗口可以高于主窗口显示,方便在顶层做一些UI效果。但是后续的上下文调用处理很麻烦。
三、解决方案:
1、不更换子窗口的情况,UI调用处的上下文获取需要进行修改,可以将上下文获取的逻辑进行删除,通过外部调用方传入上下文的形式,来获取上下文。
该方案的优点是封装者不需要考虑上下文的获取,UI显示也会符合预期。 缺点就是暴露的参数多了一个,并且有时候调用方获取上下文可能也不方便,如果是多层逻辑调用,那就要穿透式新增上下文参数了,改动也比较大。
2、更换子窗口,使用其他容器方案来实现展示在主窗口层级之上的效果。例如浮层OverlayManager。
