更新记录

1.0.0(2025-12-01)

tsw-floating-uts 悬浮窗插件

插件介绍

这是一个支持系统级和应用内悬浮窗的 uni-app UTS 插件,专为 Android 平台优化设计。该插件提供了灵活的悬浮窗解决方案,让开发者能够轻松实现各种场景下的悬浮窗需求。

核心功能特性

  • 系统级悬浮窗:需要悬浮窗权限,可在应用后台和前台显示
  • 应用内悬浮窗:不需要特殊权限,仅在应用内显示
  • 多内容类型支持:支持文本、图片两种种内容类型
  • 智能拖拽功能:支持悬浮窗拖拽,并具备自动吸附到屏幕边缘的特性
  • 完善的事件处理:支持点击事件回调和拖动结束回调
  • 优雅的错误处理:提供详细的错误日志和状态返回
  • 后台交互支持:应用在后台时,通过 Scheme 机制实现点击悬浮窗唤醒应用

技术实现原理

系统级悬浮窗实现

系统级悬浮窗使用 Android 的 WindowManager 服务和 TYPE_APPLICATION_OVERLAY 窗口类型(Android 8.0+)或 TYPE_PHONE 窗口类型(Android 8.0 以下)实现。核心实现包括:

  • 使用前台服务(Foreground Service)确保悬浮窗在应用退到后台时仍能正常显示
  • 通过 WindowManager.LayoutParams 控制悬浮窗的显示位置、大小和层级
  • 使用触摸监听器实现拖拽功能和点击事件处理
  • 实现自动吸附到屏幕边缘的功能,提升用户体验

应用内悬浮窗实现

应用内悬浮窗使用 TYPE_APPLICATION_PANEL 窗口类型实现,仅在应用内显示,具有以下特点:

  • 不需要特殊权限
  • 应用切换到后台时自动隐藏
  • 实现方式更轻量,性能开销更小
  • 适合仅在应用内需要悬浮提示的场景

安装方式

方法一:通过插件市场安装

在 uni-app 项目中,通过 HBuilderX 插件市场搜索 "tsw-floating-uts" 并安装。

方法二:手动安装

直接复制 uni_modules/tsw-floating-uts 目录到您的 uni-app 项目的 uni_modules 目录下。

权限配置

系统级悬浮窗权限配置

使用系统级悬浮窗需要在 AndroidManifest.xml 中添加以下权限:

<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 如果使用前台服务,还需要添加以下权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

权限请求流程

  1. 调用 checkPermission() 方法检查是否已获得悬浮窗权限
  2. 如果未获得权限,调用 requestPermission() 方法请求用户授权
  3. 授权成功后,即可使用系统级悬浮窗功能

接口说明

1. 权限相关接口

// 检查是否拥有悬浮窗权限
export function checkPermission(): boolean;

// 请求悬浮窗权限
export function requestPermission(): void;

2. 系统级悬浮窗接口

// 显示系统级悬浮窗
export function showFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 更新系统级悬浮窗内容
export function updateFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 隐藏系统级悬浮窗
export function hideFloatingWindow(callback?: TswFloatingUtsCallback): void;

// 设置系统级悬浮窗点击监听器
// 注意:应用在前台时直接触发回调,在后台时通过Scheme打开应用
export function setFloatingWindowClickListener(callback?: TswFloatingUtsCallback): void;

3. 应用内悬浮窗接口

// 显示应用内悬浮窗(不需要系统权限)
export function showInAppFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 隐藏应用内悬浮窗
export function hideInAppFloatingWindow(callback?: TswFloatingUtsCallback): void;

// 设置应用内悬浮窗点击监听器
export function setInAppFloatingWindowClickListener(callback?: TswFloatingUtsCallback): void;

// 设置应用内悬浮窗拖动结束监听器
export function setInAppFloatingWindowDragEndListener(callback?: TswFloatingUtsCallback): void;

// 模拟应用内悬浮窗拖拽 更新应用内悬浮窗位置
export function simulateInAppFloatingWindowDrag(x: number, y: number, callback?: TswFloatingUtsCallback): void;

4. 数据类型定义

// 接口返回结果类型
export type TswFloatingUtsResult = {
  code: number; // 0表示成功,-1表示失败
  data: any; // 结果数据
};

// 回调函数类型
export type TswFloatingUtsCallback = (result: TswFloatingUtsResult) => void;

// 内容数据类型
export type ContentData = {
  // 文本相关属性
  text?: string; // 文本内容
  textSize?: number; // 文本大小
  textColor?: string; // 文本颜色

  // 样式相关属性
  backgroundColor?: string; // 背景颜色
  cornerRadius?: number; // 圆角大小

  // 资源相关属性
  resourceId?: number; // 图片资源ID(用于本地资源)
  imageUrl?: string; // 图片URL
  videoUrl?: string; // 视频URL
};

// 悬浮窗配置选项
export type FloatingWindowOptions = {
  contentType: string; // 内容类型:"text", "image", "video"
  contentData: ContentData; // 内容数据
  width?: number; // 宽度,默认200
  height?: number; // 高度,默认100
  x?: number; // X坐标,默认100
  y?: number; // Y坐标,默认200
  draggable?: boolean; // 是否可拖拽,默认true
};

// 数据对象
export type UTSJSONObject = Record<string, any>;

使用示例

1. 系统级悬浮窗完整使用流程

import { checkPermission, requestPermission, showFloatingWindow, setFloatingWindowClickListener } from '@/uni_modules/tsw-floating-uts';

// 检查并请求权限
function setupSystemFloatingWindow() {
  const hasPermission = checkPermission();

  if (hasPermission) {
    showSystemFloatingWindow();
  } else {
    // 请求权限
    requestPermission();

    // 可以在一定时间后再次检查权限
    setTimeout(() => {
      const newPermissionStatus = checkPermission();
      if (newPermissionStatus) {
        showSystemFloatingWindow();
      } else {
        uni.showToast({
          title: '请在设置中开启悬浮窗权限',
          icon: 'none'
        });
      }
    }, 1000);
  }
}

// 显示系统级悬浮窗
function showSystemFloatingWindow() {
  showFloatingWindow({
    width: 250,
    height: 100,
    x: 50,
    y: 150,
    draggable: true,
    contentType: 'text',
    contentData: {
      text: '系统级悬浮窗示例',
      textSize: 16,
      textColor: '#FFFFFF',
      backgroundColor: '#007AFF',
      cornerRadius: 20
    }
  }, (res) => {
    if (res.code === 0) {
      console.log('系统级悬浮窗显示成功');
      setClickListener();
    } else {
      console.error('系统级悬浮窗显示失败:', res.data.message);
    }
  });
}

// 设置点击监听器
function setClickListener() {
  setFloatingWindowClickListener((res) => {
    console.log('系统悬浮窗被点击,来源:', res.data.from);
    // 应用在前台时,这里的代码会直接执行
    uni.showToast({
      title: '悬浮窗被点击',
      icon: 'none'
    });
  });
}

// 在页面显示时调用
onShow() {
  setupSystemFloatingWindow();
}

// 在页面卸载时隐藏悬浮窗
onUnload() {
  hideFloatingWindow();
}

2. 应用内悬浮窗使用示例

import { showInAppFloatingWindow, setInAppFloatingWindowClickListener, setInAppFloatingWindowDragEndListener } from '@/uni_modules/tsw-floating-uts';

// 显示应用内悬浮窗(图片类型)
function showImageFloatingWindow() {
  showInAppFloatingWindow({
    width: 120,
    height: 120,
    x: 200,
    y: 300,
    draggable: true,
    contentType: 'image',
    contentData: {
      imageUrl: '/static/logo.png',
      cornerRadius: 20
    }
  }, (res) => {
    console.log(res.data.message);
  });
}

// 配置事件监听器
function setupListeners() {
  // 设置点击监听器
  setInAppFloatingWindowClickListener((res) => {
    console.log('应用内悬浮窗被点击');
    uni.showToast({
      title: '点击了图片悬浮窗',
      icon: 'none'
    });
  });

  // 设置拖动结束监听器
  setInAppFloatingWindowDragEndListener((res) => {
    console.log(`悬浮窗拖动到位置: (${res.data.x}, ${res.data.y})`);
    // 可以在这里保存悬浮窗位置,下次打开时恢复
    saveFloatingWindowPosition(res.data.x, res.data.y);
  });
}

// 保存悬浮窗位置
function saveFloatingWindowPosition(x, y) {
  uni.setStorageSync('floatingWindowPosition', {
    x: x,
    y: y
  });
}

// 在页面显示时初始化
onShow() {
  showImageFloatingWindow();
  setupListeners();
}

// 在页面卸载时清理资源
onUnload() {
  hideInAppFloatingWindow();
}

3. 视频悬浮窗使用示例

import { showInAppFloatingWindow } from '@/uni_modules/tsw-floating-uts';

// 显示视频悬浮窗
function showVideoFloatingWindow() {
  showInAppFloatingWindow({
    width: 300,
    height: 200,
    x: 100,
    y: 200,
    draggable: true,
    contentType: 'video',
    contentData: {
      videoUrl: 'https://example.com/sample-video.mp4',
      cornerRadius: 15
    }
  }, (res) => {
    if (res.code === 0) {
      console.log('视频悬浮窗显示成功');
    } else {
      console.error('视频悬浮窗显示失败:', res.data.message);
    }
  });
}

4. 高级使用场景 - 复杂布局悬浮窗

以下示例展示如何实现一个带计数和操作按钮的复杂布局悬浮窗:

import { showInAppFloatingWindow, setInAppFloatingWindowClickListener } from '@/uni_modules/tsw-floating-uts';

// 模拟消息计数
let messageCount = 5;

// 显示带计数的通知悬浮窗
function showNotificationFloatingWindow() {
  updateNotificationWindow();
}

// 更新通知悬浮窗内容
function updateNotificationWindow() {
  // 使用HTML模板生成悬浮窗内容
  const notificationTemplate = `
    <div style="display:flex;align-items:center;justify-content:space-between;padding:10px;background-color:#007AFF;border-radius:20px;">
      <div style="display:flex;align-items:center;">
        <div style="width:20px;height:20px;background-color:red;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-right:8px;">
          <span style="color:white;font-size:12px;">${messageCount}</span>
        </div>
        <span style="color:white;">新消息</span>
      </div>
      <div style="background-color:white;color:#007AFF;padding:4px 8px;border-radius:12px;">查看</div>
    </div>
  `;

  // 注意:当前插件版本不直接支持HTML内容,这里演示的是一个概念,实际实现需要分步骤
  showInAppFloatingWindow({
    width: 180,
    height: 60,
    x: 100,
    y: 100,
    draggable: true,
    contentType: 'image',
    // 实际项目中,可以将HTML模板转换为图片URL
    contentData: {
      imageUrl: '/static/notification-icon.png',
      cornerRadius: 20
    }
  }, (res) => {
    if (res.code === 0) {
      console.log('通知悬浮窗显示成功');
      setupNotificationClickListener();
    }
  });
}

// 设置通知点击监听器
function setupNotificationClickListener() {
  setInAppFloatingWindowClickListener((res) => {
    console.log('通知悬浮窗被点击');
    // 处理通知点击事件
    handleNotificationClick();
  });
}

// 处理通知点击
function handleNotificationClick() {
  // 跳转到消息页面
  uni.navigateTo({
    url: '/pages/messages/messages'
  });

  // 重置消息计数
  messageCount = 0;
}

5. 自适应内容尺寸悬浮窗

以下示例展示如何创建根据内容自适应尺寸的悬浮窗:

import { showInAppFloatingWindow } from '@/uni_modules/tsw-floating-uts';

// 显示自适应内容尺寸的悬浮窗
function showAdaptiveFloatingWindow(content) {
  // 计算内容所需的最小宽度
  const minWidth = Math.max(content.length * 12, 150); // 估算文本宽度

  showInAppFloatingWindow({
    width: minWidth,
    height: 'auto', // 自适应高度(如果插件支持)
    x: 100,
    y: 100,
    draggable: true,
    contentType: 'text',
    contentData: {
      text: content,
      textSize: 14,
      textColor: '#333333',
      backgroundColor: '#F0F0F0',
      cornerRadius: 15
    }
  }, (res) => {
    if (res.code === 0) {
      console.log('自适应悬浮窗显示成功,宽度:', minWidth);
    }
  });
}

// 使用示例
function showTips() {
  showAdaptiveFloatingWindow('这是一条提示信息,会根据内容长度自动调整宽度');
}

6. 多悬浮窗管理

在复杂应用中,可能需要同时管理多个悬浮窗:

import { showInAppFloatingWindow, hideInAppFloatingWindow } from '@/uni_modules/tsw-floating-uts';

// 悬浮窗管理器
class FloatingWindowManager {
  constructor() {
    this.floatingWindows = new Map(); // 存储不同的悬浮窗
  }

  // 显示消息通知悬浮窗
  showNotificationWindow() {
    showInAppFloatingWindow({
      width: 160,
      height: 60,
      x: 50,
      y: 100,
      draggable: true,
      contentType: 'text',
      contentData: {
        text: '新消息通知',
        backgroundColor: '#FF3B30'
      }
    }, (res) => {
      if (res.code === 0) {
        this.floatingWindows.set('notification', 'active');
      }
    });
  }

  // 显示操作快捷悬浮窗
  showQuickActionWindow() {
    showInAppFloatingWindow({
      width: 80,
      height: 80,
      x: 250,
      y: 200,
      draggable: true,
      contentType: 'image',
      contentData: {
        imageUrl: '/static/quick-action.png',
        cornerRadius: 40
      }
    }, (res) => {
      if (res.code === 0) {
        this.floatingWindows.set('quickAction', 'active');
      }
    });
  }

  // 隐藏所有悬浮窗
  hideAllWindows() {
    this.floatingWindows.forEach((value, key) => {
      hideInAppFloatingWindow(); // 注意:当前插件API可能需要调整以支持多窗口
    });
    this.floatingWindows.clear();
  }

  // 隐藏特定悬浮窗
  hideWindow(windowId) {
    if (this.floatingWindows.has(windowId)) {
      hideInAppFloatingWindow(); // 注意:当前插件API可能需要调整以支持多窗口
      this.floatingWindows.delete(windowId);
    }
  }
}

// 创建全局悬浮窗管理器实例
const windowManager = new FloatingWindowManager();

// 使用示例
function initFloatingWindows() {
  windowManager.showNotificationWindow();
  windowManager.showQuickActionWindow();
}

// 在页面卸载时隐藏所有悬浮窗
onUnload() {
  windowManager.hideAllWindows();
}

应用在后台时的处理机制

当应用处于后台时,点击系统悬浮窗会通过以下机制唤醒应用:

  1. 首先尝试使用 launchAppToForeground 方法直接将应用带到前台
  2. 如果上述方法失败,则使用 Scheme URL 方式打开应用
  3. 应用被唤醒后,可以通过以下方式处理启动参数:
// App.vue
onLaunch() {
  // 获取启动参数
  const args = plus.runtime.arguments;
  if (args && args.includes('from=floating_window')) {
    console.log('应用通过悬浮窗点击打开');
    // 这里可以执行特定操作,如显示特定页面
    this.handleFloatingWindowLaunch();
  }
}

methods: {
  handleFloatingWindowLaunch() {
    // 处理从悬浮窗打开的逻辑
    uni.showToast({
      title: '从悬浮窗打开',
      icon: 'none'
    });

    // 可以跳转到特定页面
    uni.navigateTo({
      url: '/pages/floating-callback/floating-callback'
    });
  }
}

权限和配置说明

系统级悬浮窗权限配置

在使用系统级悬浮窗前,必须完成以下配置:

  1. AndroidManifest.xml 配置
<!-- 添加悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 添加前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- 在 application 标签内添加服务声明 -->
<service 
    android:name="uts.sdk.modules.tswFloatingUts.FloatingWindowService"
    android:exported="false"
    android:foregroundServiceType="mediaProjection" />
  1. 运行时权限请求

在使用系统级悬浮窗前,必须检查并请求悬浮窗权限。

应用内悬浮窗配置

应用内悬浮窗不需要特殊权限配置,可直接使用。

技术细节与实现说明

窗口类型与层级

  • 系统级悬浮窗:使用 TYPE_APPLICATION_OVERLAY(Android 8.0+)或 TYPE_PHONE(Android 8.0 以下)
  • 应用内悬浮窗:使用 TYPE_APPLICATION_PANEL

拖动实现机制

拖动功能通过 View.OnTouchListener 实现,主要逻辑包括:

  1. ACTION_DOWN:记录初始触摸位置和悬浮窗位置
  2. ACTION_MOVE:计算偏移量,更新悬浮窗位置
  3. ACTION_UP:判断是点击还是拖动,执行相应操作

自动吸附功能

当拖动结束时,悬浮窗会根据其中心点距离屏幕左右边缘的距离,自动吸附到最近的边缘,提升用户体验。

圆角实现

对于 Android 5.0(API 21)及以上版本,使用 ViewOutlineProvider 和 clipToOutline 属性实现平滑的圆角效果。

图片加载优化

插件实现了异步图片加载机制,包括:

  • 网络图片异步加载,避免阻塞主线程
  • 图片加载失败的错误处理和默认占位符
  • 圆角裁剪应用

常见问题与解决方案

1. 系统级悬浮窗不显示

  • 权限问题:确保已在 AndroidManifest.xml 中添加权限并在运行时请求
  • 系统设置:某些手机厂商(如小米、华为等)需要在系统设置中手动开启悬浮窗权限
  • Android 版本:Android 8.0+ 需要使用 TYPE_APPLICATION_OVERLAY 窗口类型
  • 前台服务:确保前台服务正常运行并显示通知

2. 应用内悬浮窗不显示

  • 生命周期:确保在页面 onShow 生命周期中调用 showInAppFloatingWindow
  • 坐标位置:检查 x 和 y 坐标是否在屏幕范围内
  • 内容设置:确保 contentType 和 contentData 设置正确

3. 点击事件不响应

  • 应用状态:应用在后台时,点击事件通过 Scheme 打开应用,请检查 App.vue 中的处理逻辑
  • 监听器设置:确保正确设置了点击监听器,且在悬浮窗显示后设置
  • Scheme 配置:检查应用的 Scheme 配置是否正确

4. 图片加载失败

  • URL 验证:确保图片 URL 格式正确且可访问
  • 网络状态:检查设备网络连接状态
  • 权限检查:对于网络图片,确保应用有网络访问权限

5. 视频播放问题

  • 视频格式:确保视频格式受 Android VideoView 支持
  • URL 有效性:检查视频 URL 是否可访问
  • 网络状况:视频播放需要良好的网络连接

6. updateFloatingWindow 不生效

  • 悬浮窗存在性:确保悬浮窗已经通过 showFloatingWindow 显示
  • 参数完整性:确保传递了正确的 contentType 和 contentData
  • 错误处理:检查回调函数中的错误信息

性能优化建议

  1. 合理设置悬浮窗大小:避免创建过大的悬浮窗,影响系统性能
  2. 及时隐藏悬浮窗:在不需要时及时隐藏悬浮窗,释放资源
  3. 优化图片加载:对于网络图片,考虑使用图片缓存机制
  4. 减少不必要的更新:避免频繁更新悬浮窗内容
  5. 合理处理回调:在回调函数中避免执行耗时操作

更新日志

v1.1.0

  • 新增应用内悬浮窗功能(参考 FloatingX 实现)
  • 优化系统级悬浮窗的稳定性
  • 增加自动吸附到屏幕边缘功能
  • 添加拖动结束回调

v1.0.0

  • 初始版本,支持系统级悬浮窗功能
  • 支持文本、图片和视频三种内容类型
  • 实现基础的拖拽和点击功能

开发者信息

  • 作者:tsw
  • 邮箱:[2418091167@qq.com]

平台兼容性

uni-app(4.85)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - -

其他

多语言 暗黑模式 宽屏模式
× ×

1.0.0(2025-12-01)

tsw-floating-uts 悬浮窗插件

插件介绍

这是一个支持系统级和应用内悬浮窗的 uni-app UTS 插件,专为 Android 平台优化设计。该插件提供了灵活的悬浮窗解决方案,让开发者能够轻松实现各种场景下的悬浮窗需求。

核心功能特性

  • 系统级悬浮窗:需要悬浮窗权限,可在应用后台和前台显示
  • 应用内悬浮窗:不需要特殊权限,仅在应用内显示
  • 多内容类型支持:支持文本、图片两种种内容类型
  • 智能拖拽功能:支持悬浮窗拖拽,并具备自动吸附到屏幕边缘的特性
  • 完善的事件处理:支持点击事件回调和拖动结束回调
  • 优雅的错误处理:提供详细的错误日志和状态返回
  • 后台交互支持:应用在后台时,通过 Scheme 机制实现点击悬浮窗唤醒应用

技术实现原理

系统级悬浮窗实现

系统级悬浮窗使用 Android 的 WindowManager 服务和 TYPE_APPLICATION_OVERLAY 窗口类型(Android 8.0+)或 TYPE_PHONE 窗口类型(Android 8.0 以下)实现。核心实现包括:

  • 使用前台服务(Foreground Service)确保悬浮窗在应用退到后台时仍能正常显示
  • 通过 WindowManager.LayoutParams 控制悬浮窗的显示位置、大小和层级
  • 使用触摸监听器实现拖拽功能和点击事件处理
  • 实现自动吸附到屏幕边缘的功能,提升用户体验

应用内悬浮窗实现

应用内悬浮窗使用 TYPE_APPLICATION_PANEL 窗口类型实现,仅在应用内显示,具有以下特点:

  • 不需要特殊权限
  • 应用切换到后台时自动隐藏
  • 实现方式更轻量,性能开销更小
  • 适合仅在应用内需要悬浮提示的场景

安装方式

方法一:通过插件市场安装

在 uni-app 项目中,通过 HBuilderX 插件市场搜索 "tsw-floating-uts" 并安装。

方法二:手动安装

直接复制 uni_modules/tsw-floating-uts 目录到您的 uni-app 项目的 uni_modules 目录下。

权限配置

系统级悬浮窗权限配置

使用系统级悬浮窗需要在 AndroidManifest.xml 中添加以下权限:

<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 如果使用前台服务,还需要添加以下权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

权限请求流程

  1. 调用 checkPermission() 方法检查是否已获得悬浮窗权限
  2. 如果未获得权限,调用 requestPermission() 方法请求用户授权
  3. 授权成功后,即可使用系统级悬浮窗功能

接口说明

1. 权限相关接口

// 检查是否拥有悬浮窗权限
export function checkPermission(): boolean;

// 请求悬浮窗权限
export function requestPermission(): void;

2. 系统级悬浮窗接口

// 显示系统级悬浮窗
export function showFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 更新系统级悬浮窗内容
export function updateFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 隐藏系统级悬浮窗
export function hideFloatingWindow(callback?: TswFloatingUtsCallback): void;

// 设置系统级悬浮窗点击监听器
// 注意:应用在前台时直接触发回调,在后台时通过Scheme打开应用
export function setFloatingWindowClickListener(callback?: TswFloatingUtsCallback): void;

3. 应用内悬浮窗接口

// 显示应用内悬浮窗(不需要系统权限)
export function showInAppFloatingWindow(options: FloatingWindowOptions, callback?: TswFloatingUtsCallback): void;

// 隐藏应用内悬浮窗
export function hideInAppFloatingWindow(callback?: TswFloatingUtsCallback): void;

// 设置应用内悬浮窗点击监听器
export function setInAppFloatingWindowClickListener(callback?: TswFloatingUtsCallback): void;

// 设置应用内悬浮窗拖动结束监听器
export function setInAppFloatingWindowDragEndListener(callback?: TswFloatingUtsCallback): void;

// 模拟应用内悬浮窗拖拽 更新应用内悬浮窗位置
export function simulateInAppFloatingWindowDrag(x: number, y: number, callback?: TswFloatingUtsCallback): void;

4. 数据类型定义

// 接口返回结果类型
export type TswFloatingUtsResult = {
  code: number; // 0表示成功,-1表示失败
  data: any; // 结果数据
};

// 回调函数类型
export type TswFloatingUtsCallback = (result: TswFloatingUtsResult) => void;

// 内容数据类型
export type ContentData = {
  // 文本相关属性
  text?: string; // 文本内容
  textSize?: number; // 文本大小
  textColor?: string; // 文本颜色

  // 样式相关属性
  backgroundColor?: string; // 背景颜色
  cornerRadius?: number; // 圆角大小

  // 资源相关属性
  resourceId?: number; // 图片资源ID(用于本地资源)
  imageUrl?: string; // 图片URL
  videoUrl?: string; // 视频URL
};

// 悬浮窗配置选项
export type FloatingWindowOptions = {
  contentType: string; // 内容类型:"text", "image", "video"
  contentData: ContentData; // 内容数据
  width?: number; // 宽度,默认200
  height?: number; // 高度,默认100
  x?: number; // X坐标,默认100
  y?: number; // Y坐标,默认200
  draggable?: boolean; // 是否可拖拽,默认true
};

// 数据对象
export type UTSJSONObject = Record<string, any>;

使用示例

1. 系统级悬浮窗完整使用流程

import { checkPermission, requestPermission, showFloatingWindow, setFloatingWindowClickListener } from '@/uni_modules/tsw-floating-uts';

// 检查并请求权限
function setupSystemFloatingWindow() {
  const hasPermission = checkPermission();

  if (hasPermission) {
    showSystemFloatingWindow();
  } else {
    // 请求权限
    requestPermission();

    // 可以在一定时间后再次检查权限
    setTimeout(() => {
      const newPermissionStatus = checkPermission();
      if (newPermissionStatus) {
        showSystemFloatingWindow();
      } else {
        uni.showToast({
          title: '请在设置中开启悬浮窗权限',
          icon: 'none'
        });
      }
    }, 1000);
  }
}

// 显示系统级悬浮窗
function showSystemFloatingWindow() {
  showFloatingWindow({
    width: 250,
    height: 100,
    x: 50,
    y: 150,
    draggable: true,
    contentType: 'text',
    contentData: {
      text: '系统级悬浮窗示例',
      textSize: 16,
      textColor: '#FFFFFF',
      backgroundColor: '#007AFF',
      cornerRadius: 20
    }
  }, (res) => {
    if (res.code === 0) {
      console.log('系统级悬浮窗显示成功');
      setClickListener();
    } else {
      console.error('系统级悬浮窗显示失败:', res.data.message);
    }
  });
}

// 设置点击监听器
function setClickListener() {
  setFloatingWindowClickListener((res) => {
    console.log('系统悬浮窗被点击,来源:', res.data.from);
    // 应用在前台时,这里的代码会直接执行
    uni.showToast({
      title: '悬浮窗被点击',
      icon: 'none'
    });
  });
}

// 在页面显示时调用
onShow() {
  setupSystemFloatingWindow();
}

// 在页面卸载时隐藏悬浮窗
onUnload() {
  hideFloatingWindow();
}

2. 应用内悬浮窗使用示例

import { showInAppFloatingWindow, setInAppFloatingWindowClickListener, setInAppFloatingWindowDragEndListener } from '@/uni_modules/tsw-floating-uts';

// 显示应用内悬浮窗(图片类型)
function showImageFloatingWindow() {
  showInAppFloatingWindow({
    width: 120,
    height: 120,
    x: 200,
    y: 300,
    draggable: true,
    contentType: 'image',
    contentData: {
      imageUrl: '/static/logo.png',
      cornerRadius: 20
    }
  }, (res) => {
    console.log(res.data.message);
  });
}

// 配置事件监听器
function setupListeners() {
  // 设置点击监听器
  setInAppFloatingWindowClickListener((res) => {
    console.log('应用内悬浮窗被点击');
    uni.showToast({
      title: '点击了图片悬浮窗',
      icon: 'none'
    });
  });

  // 设置拖动结束监听器
  setInAppFloatingWindowDragEndListener((res) => {
    console.log(`悬浮窗拖动到位置: (${res.data.x}, ${res.data.y})`);
    // 可以在这里保存悬浮窗位置,下次打开时恢复
    saveFloatingWindowPosition(res.data.x, res.data.y);
  });
}

// 保存悬浮窗位置
function saveFloatingWindowPosition(x, y) {
  uni.setStorageSync('floatingWindowPosition', {
    x: x,
    y: y
  });
}

// 在页面显示时初始化
onShow() {
  showImageFloatingWindow();
  setupListeners();
}

// 在页面卸载时清理资源
onUnload() {
  hideInAppFloatingWindow();
}

权限和配置说明

系统级悬浮窗权限配置

在使用系统级悬浮窗前,必须完成以下配置:

  1. AndroidManifest.xml 配置
<!-- 添加悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 添加前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- 在 application 标签内添加服务声明 -->
<service 
    android:name="uts.sdk.modules.tswFloatingUts.FloatingWindowService"
    android:exported="false"
    android:foregroundServiceType="mediaProjection" />
  1. 运行时权限请求

在使用系统级悬浮窗前,必须检查并请求悬浮窗权限。

应用内悬浮窗配置

应用内悬浮窗不需要特殊权限配置,可直接使用。

技术细节与实现说明

窗口类型与层级

  • 系统级悬浮窗:使用 TYPE_APPLICATION_OVERLAY(Android 8.0+)或 TYPE_PHONE(Android 8.0 以下)
  • 应用内悬浮窗:使用 TYPE_APPLICATION_PANEL

拖动实现机制

拖动功能通过 View.OnTouchListener 实现,主要逻辑包括:

  1. ACTION_DOWN:记录初始触摸位置和悬浮窗位置
  2. ACTION_MOVE:计算偏移量,更新悬浮窗位置
  3. ACTION_UP:判断是点击还是拖动,执行相应操作

自动吸附功能

当拖动结束时,悬浮窗会根据其中心点距离屏幕左右边缘的距离,自动吸附到最近的边缘,提升用户体验。

圆角实现

对于 Android 5.0(API 21)及以上版本,使用 ViewOutlineProvider 和 clipToOutline 属性实现平滑的圆角效果。

图片加载优化

插件实现了异步图片加载机制,包括:

  • 网络图片异步加载,避免阻塞主线程
  • 图片加载失败的错误处理和默认占位符
  • 圆角裁剪应用

常见问题与解决方案

1. 系统级悬浮窗不显示

  • 权限问题:确保已在 AndroidManifest.xml 中添加权限并在运行时请求
  • 系统设置:某些手机厂商(如小米、华为等)需要在系统设置中手动开启悬浮窗权限
  • Android 版本:Android 8.0+ 需要使用 TYPE_APPLICATION_OVERLAY 窗口类型
  • 前台服务:确保前台服务正常运行并显示通知

2. 应用内悬浮窗不显示

  • 生命周期:确保在页面 onShow 生命周期中调用 showInAppFloatingWindow
  • 坐标位置:检查 x 和 y 坐标是否在屏幕范围内
  • 内容设置:确保 contentType 和 contentData 设置正确

3. 点击事件不响应

  • 应用状态:应用在后台时,点击事件通过 Scheme 打开应用,请检查 App.vue 中的处理逻辑
  • 监听器设置:确保正确设置了点击监听器,且在悬浮窗显示后设置
  • Scheme 配置:检查应用的 Scheme 配置是否正确

4. 图片加载失败

  • URL 验证:确保图片 URL 格式正确且可访问
  • 网络状态:检查设备网络连接状态
  • 权限检查:对于网络图片,确保应用有网络访问权限

5. updateFloatingWindow 不生效

  • 悬浮窗存在性:确保悬浮窗已经通过 showFloatingWindow 显示
  • 参数完整性:确保传递了正确的 contentType 和 contentData
  • 错误处理:检查回调函数中的错误信息

性能优化建议

  1. 合理设置悬浮窗大小:避免创建过大的悬浮窗,影响系统性能
  2. 及时隐藏悬浮窗:在不需要时及时隐藏悬浮窗,释放资源
  3. 优化图片加载:对于网络图片,考虑使用图片缓存机制
  4. 减少不必要的更新:避免频繁更新悬浮窗内容
  5. 合理处理回调:在回调函数中避免执行耗时操作

更新日志

v1.1.0

  • 新增应用内悬浮窗功能(参考 FloatingX 实现)
  • 优化系统级悬浮窗的稳定性
  • 增加自动吸附到屏幕边缘功能
  • 添加拖动结束回调

v1.0.0

  • 初始版本,支持系统级悬浮窗功能
  • 支持文本、图片和视频三种内容类型
  • 实现基础的拖拽和点击功能

开发者信息

  • 作者:tsw
  • 邮箱:[2418091167@qq.com]

隐私、权限声明

1. 本插件需要申请的系统权限列表:

<!-- 悬浮窗权限 --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 如果使用前台服务,还需要添加以下权限 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率: