1# 如何创建悬浮窗
2
3## 场景说明
4悬浮窗功能可以基于当前任务创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示,通常悬浮窗位于所有应用窗口之上。很多应用都具有悬浮窗的功能,常见的如视频应用的视频播放窗口,在视频应用切换到后台后,视频播放窗口还可以在前台以小窗形式继续播放。本例即为大家介绍如何开发悬浮窗。
5
6## 效果呈现
7本例效果如下:
8
9![float-window](figures/float-window.gif)
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)