1# 如何创建悬浮窗 2 3## 场景说明 4悬浮窗功能可以基于当前任务创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示,通常悬浮窗位于所有应用窗口之上。很多应用都具有悬浮窗的功能,常见的如视频应用的视频播放窗口,在视频应用切换到后台后,视频播放窗口还可以在前台以小窗形式继续播放。本例即为大家介绍如何开发悬浮窗。 5 6## 效果呈现 7本例效果如下: 8 9 10 11## 运行环境 12本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: 13 14- IDE: DevEco Studio 4.0 Beta1 15- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1) 16 17 18## 实现思路 19本例中主要涉及三项关键操作,相关实现方案如下: 20- 创建悬浮窗:使用window类的createWindow方法创建窗口,窗口类型设置为window.WindowType.TYPE_FLOAT 21- 悬浮窗可拖拽:通过gesture为窗口绑定手势事件,使用PanGesture监听拖拽手势并记录窗口位置,通过moveWindowTo方法将窗口移动到拖拽位置从而实现窗口拖拽。 22- 退出悬浮窗口:使用destroyWindow方法,销毁悬浮窗。 23 24## 开发步骤 25由于本例重点讲解悬浮窗的创建和使用,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 261. 申请权限。 27 28 创建悬浮窗需要先申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,要在module.json5文件的requestPermissions对象中进行配置,如下: 29 ```json 30 { 31 "module": { 32 "requestPermissions":[ 33 { 34 "name" : "ohos.permission.SYSTEM_FLOAT_WINDOW", 35 "usedScene": { 36 "abilities": [ 37 "EntryAbility" 38 ], 39 "when":"inuse" 40 } 41 } 42 ] 43 } 44 } 45 ``` 462. 创建悬浮窗。 47 48 使用window类的createWindow方法创建窗口,窗口类型设置为window.WindowType.TYPE_FLOAT。由于本例通过按钮的点击事件控制悬浮窗的创建和销毁,为了便于操作,本例将创建和销毁悬浮窗的操作写在自定义的方法中,以便绑定到按钮的点击时间中。 49 创建悬浮窗的操作在自定义方法createFloatWindow中实现。 50 具体代码如下: 51 ```ts 52 // 引入window类 53 import window from '@ohos.window'; 54 ... 55 // 自定义创建悬浮窗方法 56 createFloatWindow() { 57 let windowClass = null; 58 // 窗口类型设置为window.WindowType.TYPE_FLOAT 59 let config = {name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: getContext(this)}; 60 // 创建悬浮窗 61 window.createWindow(config, (err, data) => { 62 if (err.code) { 63 console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); 64 return; 65 } 66 console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); 67 windowClass = data; 68 } 69 } 70 ``` 713. 设置窗口信息。 72 73 创建悬浮窗时,可以对窗口的位置、大小、内容等进行设置。 74 具体代码如下: 75 ```ts 76 ... 77 window.createWindow(config, (err, data) => { 78 ... 79 windowClass = data; 80 // 设置悬浮窗位置 81 windowClass.moveWindowTo(300, 300, (err) => { 82 if (err.code) { 83 console.error('Failed to move the window. Cause:' + JSON.stringify(err)); 84 return; 85 } 86 console.info('Succeeded in moving the window.'); 87 }); 88 // 设置悬浮窗大小 89 windowClass.resize(500, 500, (err) => { 90 if (err.code) { 91 console.error('Failed to change the window size. Cause:' + JSON.stringify(err)); 92 return; 93 } 94 console.info('Succeeded in changing the window size.'); 95 }); 96 //为悬浮窗加载页面内容,这里可以设置在main_pages.json中配置的页面 97 windowClass.setUIContent("pages/FloatContent", (err) => { 98 if (err.code) { 99 console.error('Failed to load the content. Cause:' + JSON.stringify(err)); 100 return; 101 } 102 console.info('Succeeded in loading the content.'); 103 // 显示悬浮窗。 104 windowClass.showWindow((err) => { 105 if (err.code) { 106 console.error('Failed to show the window. Cause: ' + JSON.stringify(err)); 107 return; 108 } 109 console.info('Succeeded in showing the window.'); 110 }); 111 }); 112 }); 113 ``` 1144. 销毁悬浮窗。 115 116 使用destroyWindow方法销毁悬浮窗,为了便于通过按钮点击控制悬浮窗的销毁,我们这里将销毁逻辑写在自定义方法destroyFloatWindow中。 117 具体代码如下: 118 ```ts 119 // 定义windowClass变量,用来接收创建的悬浮窗 120 private windowClass: window.Window; 121 122 createFloatWindow() { 123 ... 124 // 创建悬浮窗。 125 window.createWindow(config, (err, data) => { 126 if (err.code) { 127 console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); 128 return; 129 } 130 console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); 131 // 用windowClass变量接收创建的悬浮窗 132 this.windowClass = data; 133 ... 134 } 135 } 136 // 自定义销毁悬浮窗方法 137 destroyFloatWindow() { 138 // 用windowClass调用destroyWindow销毁悬浮窗 139 this.windowClass.destroyWindow((err) => { 140 if (err.code) { 141 console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err)); 142 return; 143 } 144 console.info('Succeeded in destroying the window.'); 145 }); 146 } 147 ``` 1485. 构建主页面UI。 149 150 将创建悬浮窗和销毁悬浮窗绑定到对应的按钮上。 151 具体代码如下: 152 ```ts 153 ... 154 build() { 155 Row() { 156 Column() { 157 Button('创建悬浮窗') 158 .onClick(() => { 159 // 点击按钮调用创建悬浮窗方法 160 this.createFloatWindow(); 161 }) 162 Button('销毁悬浮窗') 163 .margin({top:20}) 164 .onClick(() => { 165 // 点击按钮调用销毁悬浮窗方法 166 this.destroyFloatWindow(); 167 }) 168 } 169 .width('100%') 170 } 171 .height('100%') 172 } 173 ... 174 ``` 1756. 创建悬浮窗的显示页面并实现悬浮窗可拖拽。 176 177 为页面内容绑定PanGesture拖拽事件,拖拽事件发生时获取到触摸点的位置信息,使用@Watch监听到位置变量的变化,然后调用窗口的moveWindowTo方法将窗口移动到对应位置,从而实现拖拽效果。 178 具体代码如下: 179 ```ts 180 import window from '@ohos.window'; 181 182 interface Position { 183 x: number, 184 y: number 185 } 186 @Entry 187 @Component 188 struct FloatContent { 189 @State message: string = 'float window' 190 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); 191 // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 192 @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; 193 floatWindow: window.Window 194 // 通过悬浮窗名称“floatWindow”获取到创建的悬浮窗 195 aboutToAppear() { 196 this.floatWindow = window.findWindow("floatWindow") 197 } 198 // 将悬浮窗移动到指定位置 199 moveWindow() { 200 this.floatWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); 201 } 202 203 build() { 204 Row() { 205 Column() { 206 Text(this.message) 207 .fontSize(50) 208 .fontWeight(FontWeight.Bold) 209 } 210 .width('100%') 211 } 212 .height('100%') 213 .gesture( 214 // 绑定PanGesture事件,监听拖拽动作 215 PanGesture(this.panOption) 216 .onActionStart((event: GestureEvent) => { 217 console.info('Pan start'); 218 }) 219 .onActionUpdate((event: GestureEvent) => { 220 // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition 221 this.windowPosition.x += event.offsetX; 222 this.windowPosition.y += event.offsetY; 223 }) 224 .onActionEnd(() => { 225 console.info('Pan end'); 226 }) 227 ) 228 .border({ 229 style: BorderStyle.Dotted 230 }) 231 .backgroundColor(Color.Yellow) 232 } 233 } 234 ``` 235 236## 完整代码 237本例完整代码如下: 238主窗口代码(FloatWindow.ets): 239```ts 240//FloatWindow.ets 241// 引入window类 242import window from '@ohos.window'; 243 244@Entry 245@Component 246struct FloatWindow { 247 // 定义windowClass变量,用来接收创建的悬浮窗 248 private windowClass: window.Window; 249 // 自定义创建悬浮窗方法 250 createFloatWindow() { 251 let windowClass = null; 252 // 窗口类型设置为window.WindowType.TYPE_FLOAT 253 let config = {name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: getContext(this)}; 254 // 创建悬浮窗 255 window.createWindow(config, (err, data) => { 256 if (err.code) { 257 console.error('Failed to create the floatWindow. Cause: ' + JSON.stringify(err)); 258 return; 259 } 260 console.info('Succeeded in creating the floatWindow. Data: ' + JSON.stringify(data)); 261 windowClass = data; 262 // 用windowClass变量接收创建的悬浮窗 263 this.windowClass = data; 264 // 设置悬浮窗位置 265 windowClass.moveWindowTo(300, 300, (err) => { 266 if (err.code) { 267 console.error('Failed to move the window. Cause:' + JSON.stringify(err)); 268 return; 269 } 270 console.info('Succeeded in moving the window.'); 271 }); 272 // 设置悬浮窗大小 273 windowClass.resize(500, 500, (err) => { 274 if (err.code) { 275 console.error('Failed to change the window size. Cause:' + JSON.stringify(err)); 276 return; 277 } 278 console.info('Succeeded in changing the window size.'); 279 }); 280 // 为悬浮窗加载页面内容,这里可以设置在main_pages.json中配置的页面 281 windowClass.setUIContent("pages/FloatContent", (err) => { 282 if (err.code) { 283 console.error('Failed to load the content. Cause:' + JSON.stringify(err)); 284 return; 285 } 286 console.info('Succeeded in loading the content.'); 287 // 显示悬浮窗。 288 windowClass.showWindow((err) => { 289 if (err.code) { 290 console.error('Failed to show the window. Cause: ' + JSON.stringify(err)); 291 return; 292 } 293 console.info('Succeeded in showing the window.'); 294 }); 295 }); 296 297 }); 298 } 299 // 自定义销毁悬浮窗方法 300 destroyFloatWindow() { 301 // 用windowClass调用destroyWindow销毁悬浮窗 302 this.windowClass.destroyWindow((err) => { 303 if (err.code) { 304 console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err)); 305 return; 306 } 307 console.info('Succeeded in destroying the window.'); 308 }); 309 } 310 311 build() { 312 Row() { 313 Column() { 314 Button('创建悬浮窗') 315 .backgroundColor('#F9C449') 316 .onClick(() => { 317 // 点击按钮调用创建悬浮窗方法 318 this.createFloatWindow(); 319 }) 320 Button('销毁悬浮窗') 321 .margin({top:20}) 322 .backgroundColor('#F9C449') 323 .onClick(() => { 324 // 点击按钮调用销毁悬浮窗方法 325 this.destroyFloatWindow(); 326 }) 327 } 328 .width('100%') 329 } 330 .height('100%') 331 } 332} 333``` 334悬浮窗内容页代码(FloatContent.ets): 335```ts 336//FloatContent.ets 337import window from '@ohos.window'; 338 339interface Position { 340 x: number, 341 y: number 342} 343@Entry 344@Component 345struct FloatContent { 346 @State message: string = 'float window' 347 private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); 348 // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口 349 @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 }; 350 floatWindow: window.Window 351 // 通过悬浮窗名称“floatWindow”获取到创建的悬浮窗 352 aboutToAppear() { 353 this.floatWindow = window.findWindow("floatWindow") 354 } 355 // 将悬浮窗移动到指定位置 356 moveWindow() { 357 this.floatWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y); 358 } 359 360 build() { 361 Row() { 362 Column() { 363 Text(this.message) 364 .fontSize(30) 365 .fontColor(Color.White) 366 .fontWeight(FontWeight.Bold) 367 } 368 .width('100%') 369 } 370 .height('100%') 371 .gesture( 372 // 绑定PanGesture事件,监听拖拽动作 373 PanGesture(this.panOption) 374 .onActionStart((event: GestureEvent) => { 375 console.info('Pan start'); 376 }) 377 // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition 378 .onActionUpdate((event: GestureEvent) => { 379 this.windowPosition.x += event.offsetX; 380 this.windowPosition.y += event.offsetY; 381 }) 382 .onActionEnd(() => { 383 console.info('Pan end'); 384 }) 385 ) 386 .border({ 387 style: BorderStyle.Dotted 388 }) 389 .backgroundColor("#E8A49C") 390 } 391} 392``` 393## 参考 394- [管理应用窗口(Stage模型)](../application-dev/windowmanager/application-window-stage.md) 395- [@ohos.window (窗口)](../application-dev/reference/apis-arkui/js-apis-window.md) 396- [单一手势](../application-dev/ui/arkts-gesture-events-single-gesture.md) 397- [@Watch:状态变量更改通知](../application-dev/quick-start/arkts-watch.md)