Android实现悬浮窗(附带源码)

一、项目介绍

在 Android 应用中,**悬浮窗(Floating Window)**又称系统级别的“悬浮球”或“悬浮视图”,可以在所有应用界面之上悬浮显示,常用于:

  • 悬浮工具:音乐控制、悬浮记事、翻译小工具

  • 悬浮通知:来电通知、消息提醒

  • 悬浮快捷入口:一键拍照、一键拨号

  • 辅助功能:屏幕取色、辅助点击

一个完整的浮窗功能,需要解决以下关键点:

  1. 权限申请:在 Android 6.0+ 需获取 ACTION_MANAGE_OVERLAY_PERMISSION

  2. WindowManager:以 TYPE_APPLICATION_OVERLAY 添加悬浮视图

  3. View 生命周期:服务(Service)中管理悬浮视图的创建与销毁

  4. 触摸事件:支持拖拽、点击、缩放等交互

  5. 布局与样式:自定义布局、支持多种内容

  6. 性能与节电:保证悬浮视图不影响电量与流畅度

  7. 兼容性:兼容不同 Android 版本与设备

本文将从原理剖析实现思路完整代码示例代码解读性能优化扩展思考常见问答等方面进行深度阐述,助你快速掌握 Android 悬浮窗开发的全流程方案。


二、相关知识

  1. SYSTEM_ALERT_WINDOW 权限

    • Android 6.0(API 23)后需用户手动授予“在其他应用上层显示”权限,跳转到设置页面开启。

  2. WindowManager.LayoutParams 类型

    • TYPE_APPLICATION_OVERLAY(API 26+)替代旧的 TYPE_PHONE/TYPE_SYSTEM_ALERT,需配合 SYSTEM_ALERT_WINDOW

  3. WindowManager

    • 通过 getSystemService(WINDOW_SERVICE) 获取,使用 addView()updateViewLayout()removeView() 管理悬浮视图。

  4. Service

    • 通常在 Service 中管理悬浮视图,防止被 Activity 生命周期影响;前台 Service 可提高存活优先级。

  5. 触摸拦截与事件分发

    • 悬浮视图需要自行处理 onTouchEvent() 或在自定义 View 中拦截 ACTION_DOWN/MOVE/UP 以实现拖拽与点击。

  6. 布局文件

    • 可自定义 XML 布局,支持 ImageView、TextView、Button 或任意组合。

  7. 电量与性能优化

    • 悬浮视图一般只需在用户交互时绘制,保持轻量;避免在 onDraw() 中做大量运算。


三、实现思路

  1. 界面入口

    • 在主 Activity 提供按钮,一键进入悬浮窗权限设置并启动 FloatWindowService

  2. 权限申请

    • 检测 Settings.canDrawOverlays(),若未开启则调用 ACTION_MANAGE_OVERLAY_PERMISSION

  3. 服务管理

    • FloatWindowServiceonCreate() 中创建并添加悬浮视图,在 onDestroy() 中移除。

    • 可提升为前台 Service,确保长期存活。

  4. 浮窗视图

    • 自定义 FloatWindowView(可继承 FrameLayout 或纯 View),在内部加载布局 float_window.xml

    • onTouchEvent() 中计算偏移量,调用 windowManager.updateViewLayout() 更新位置,实现拖拽。

    • 处理点击事件,如打开主界面、显示菜单等。

  5. 布局与样式

    • float_window.xml 定义圆形、半透明、带阴影的浮窗样式;支持在布局中放置任意控件。

  6. 资源释放

    • Service.onDestroy() 或用户主动退出时,调用 windowManager.removeView(),避免内存泄漏。


四、环境与依赖

// app/build.gradle
plugins {
  id 'com.android.application'
  id 'kotlin-android'
}

android {
  compileSdk 34
  defaultConfig {
    applicationId "com.example.floatingwindow"
    minSdk 21
    targetSdk 34
  }
  buildFeatures.viewBinding true
}

dependencies {
  implementation "androidx.appcompat:appcompat:1.6.1"
  implementation "androidx.core:core-ktx:1.10.1"
}

五、整合代码

// =======================================================
// 文件:AndroidManifest.xml
// 描述:声明 Service,并申请 SYSTEM_ALERT_WINDOW 权限
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.floatingwindow">

  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  <application
      android:name=".App"
      android:theme="@style/Theme.FloatingWindow">
    <activity android:name=".MainActivity"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <service android:name=".FloatWindowService"
        android:exported="false"/>
  </application>
</manifest>

// =======================================================
// 文件:App.kt
// 描述:Application,初始化通用配置(如通知渠道)
// =======================================================
package com.example.floatingwindow

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build

class App : Application() {
  companion object {
    const val CHANNEL_ID = "floating_service_channel"
  }
  override fun onCreate() {
    super.onCreate()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      val nm = getSystemService(NotificationManager::class.java)
      nm.createNotificationChannel(
        NotificationChannel(
          CHANNEL_ID,
          "悬浮窗服务",
          NotificationManager.IMPORTANCE_LOW
        )
      )
    }
  }
}

// =======================================================
// 文件:res/values/themes.xml
// 描述:应用主题
// =======================================================
<resources>
  <style name="Theme.FloatingWindow" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="colorPrimary">@color/purple_500</item>
    <item name="colorOnPrimary">@android:color/white</item>
  </style>
</resources>

// =======================================================
// 文件:res/layout/activity_main.xml
// 描述:主界面,包含按钮用于授权及启动悬浮窗服务
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:gravity="center"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:padding="24dp">

  <Button
      android:id="@+id/btnRequest"
      android:layout_width="wrap_content" android:layout_height="wrap_content"
      android:text="申请悬浮窗权限"/>

  <Button
      android:id="@+id/btnStartService"
      android:layout_width="wrap_content" android:layout_height="wrap_content"
      android:text="启动悬浮窗"
      android:layout_marginTop="16dp"/>
</LinearLayout>

// =======================================================
// 文件:MainActivity.kt
// 描述:请求权限并启动/停止悬浮窗服务
// =======================================================
package com.example.floatingwindow

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.example.floatingwindow.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.btnRequest.setOnClickListener {
      if (Settings.canDrawOverlays(this)) {
        Toast.makeText(this, "已拥有悬浮窗权限", Toast.LENGTH_SHORT).show()
      } else {
        val intent = Intent(
          Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
          Uri.parse("package:$packageName")
        )
        startActivity(intent)
      }
    }

    binding.btnStartService.setOnClickListener {
      if (!Settings.canDrawOverlays(this)) {
        Toast.makeText(this, "请先授予悬浮窗权限", Toast.LENGTH_SHORT).show()
        return@setOnClickListener
      }
      Intent(this, FloatWindowService::class.java).also { intent ->
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          startForegroundService(intent)
        } else {
          startService(intent)
        }
      }
      finish()  // 启动后可关闭界面
    }
  }
}

// =======================================================
// 文件:FloatWindowService.kt
// 描述:前台 Service,管理悬浮窗视图创建与销毁
// =======================================================
package com.example.floatingwindow

import android.app.*
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.*
import androidx.core.app.NotificationCompat

class FloatWindowService : Service() {
  private lateinit var windowManager: WindowManager
  private lateinit var floatView: FloatWindowView
  private lateinit var layoutParams: WindowManager.LayoutParams

  override fun onCreate() {
    super.onCreate()
    windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
    floatView = FloatWindowView(this)

    // 配置悬浮窗 LayoutParams
    val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
      WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    else WindowManager.LayoutParams.TYPE_PHONE

    layoutParams = WindowManager.LayoutParams().apply {
      this.type = type
      format = PixelFormat.TRANSLUCENT
      flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
              WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
      width = WindowManager.LayoutParams.WRAP_CONTENT
      height = WindowManager.LayoutParams.WRAP_CONTENT
      gravity = Gravity.TOP or Gravity.START
      x = 100
      y = 200
    }
    windowManager.addView(floatView, layoutParams)

    // 作为前台服务,避免被系统回收
    val notification = NotificationCompat.Builder(this, App.CHANNEL_ID)
      .setContentTitle("悬浮窗服务运行中")
      .setSmallIcon(R.drawable.ic_notification)
      .setOngoing(true)
      .build()
    startForeground(1, notification)
  }

  override fun onDestroy() {
    super.onDestroy()
    windowManager.removeViewImmediate(floatView)
  }

  override fun onBind(intent: Intent?): IBinder? = null
}

// =======================================================
// 文件:FloatWindowView.kt
// 描述:自定义悬浮视图,支持拖拽与点击
// =======================================================
package com.example.floatingwindow

import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import com.example.floatingwindow.databinding.FloatWindowLayoutBinding

class FloatWindowView(context: Context) : FrameLayout(context) {
  private val binding = FloatWindowLayoutBinding.inflate(LayoutInflater.from(context), this, true)
  private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
  private val params = layoutParams as WindowManager.LayoutParams

  private var lastX = 0
  private var lastY = 0
  private var downX = 0
  private var downY = 0

  init {
    // 点击事件
    binding.root.setOnClickListener {
      // 示例:点击弹 Toast,可替换为打开 Activity 等操作
      android.widget.Toast.makeText(context, "悬浮窗被点击", Toast.LENGTH_SHORT).show()
    }
    // 触摸拖拽
    binding.root.setOnTouchListener { v, event ->
      when (event.action) {
        MotionEvent.ACTION_DOWN -> {
          downX = event.rawX.toInt()
          downY = event.rawY.toInt()
          lastX = params.x
          lastY = params.y
          true
        }
        MotionEvent.ACTION_MOVE -> {
          val dx = event.rawX.toInt() - downX
          val dy = event.rawY.toInt() - downY
          params.x = lastX + dx
          params.y = lastY + dy
          windowManager.updateViewLayout(this, params)
          true
        }
        else -> false
      }
    }
  }
}

// =======================================================
// 文件:res/layout/float_window_layout.xml
// 描述:悬浮窗内部布局,示例一个圆形 ImageView
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@drawable/float_window_bg"
    android:layout_width="60dp"
    android:layout_height="60dp"
    android:padding="4dp">

  <ImageView
      android:id="@+id/imageView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:src="@drawable/ic_floating"
      android:scaleType="centerCrop"/>
</FrameLayout>

// =======================================================
// 文件:res/drawable/float_window_bg.xml
// 描述:圆形半透明背景
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
  <solid android:color="#80FFFFFF"/>
  <size android:width="60dp" android:height="60dp"/>
  <corners android:radius="30dp"/>
</shape>

六、代码解读

  1. 权限与启动

    • MainActivity 通过 Settings.canDrawOverlays() 检测悬浮窗权限,若未授予则引导至系统设置页;

    • 授权后启动 FloatWindowService,并结束 Activity,保持界面整洁。

  2. 前台服务

    • FloatWindowServiceonCreate() 中调用 startForeground(),将服务提升至前台,避免系统回收;

    • onDestroy() 中移除悬浮视图,防止内存泄漏。

  3. WindowManager 与 LayoutParams

    • 使用 TYPE_APPLICATION_OVERLAY(Android O+)或兼容旧版本的 TYPE_PHONE

    • FLAG_NOT_FOCUSABLE 保证点击事件只传递给浮窗内部,背后界面仍可交互;

    • FLAG_LAYOUT_NO_LIMITS 允许浮窗拖出屏幕边界部分位置。

  4. 自定义视图 FloatWindowView

    • 在构造中使用 ViewBinding 加载 float_window_layout.xml,方便维护与扩展;

    • 通过 setOnTouchListener 实现拖拽:记录初始按下坐标与悬浮窗初始偏移,计算位移后调用 updateViewLayout()

    • 通过 setOnClickListener 实现点击事件。

  5. 布局与样式

    • float_window_layout.xml 定义 FrameLayout 容器,内部放置 ImageView

    • float_window_bg.xml 使用 <shape> 定义半透明白色圆形背景,配合 padding 调整大小。


七、性能与优化

  1. 轻量化

    • 浮窗仅承载一个小视图,避免在视图中进行大量更新或动画;

  2. 电量控制

    • 若浮窗需要定时刷新,可使用 Handler 并在不可见时暂停;

  3. 优先级管理

    • 前台服务保证系统不轻易回收,但也会消耗少量电量,需视场景决定是否使用;

  4. 多浮窗管理

    • 若有多个悬浮视图,可在 Service 中维护一组 ViewLayoutParams,统一管理与更新。


八、项目总结与扩展思路

本文从权限管理WindowManagerService自定义视图交互等方面,完整演示了 Android 悬浮窗的实现方法,并整合出一套可复用的模板。后续可进一步扩展:

  • 多功能菜单:点击浮窗展开多按钮选项菜单

  • 缩放与动画:支持双指缩放、长按拖拽/缩放结合、点击时弹出动画

  • 状态显示:在浮窗内部动态展示网络、时间、电量等信息

  • 多窗格布局:同时支持多个不同大小和功能的悬浮窗

  • 可配置化 SDK:将本实现打包成库,提供一键集成的接口


九、常见问题解答(FAQ)

Q1:为什么启动后看不到浮窗?

  • 检查是否已经通过“在其他应用上层显示”权限;

  • 确认 LayoutParams.type 是否设置为 TYPE_APPLICATION_OVERLAY(API ≥26)或兼容值。

Q2:悬浮窗无法响应点击?

  • 确认 FLAG_NOT_FOCUSABLE 是否设置;若需要拦截背后点击,请移除该 flag。

Q3:为何浮窗内容不更新?

  • 浮窗本质上是普通 View,更新数据后需要调用对应子 View 的刷新方法,如 invalidate()

Q4:如何关闭浮窗?

  • 在 Service 中调用 stopForeground(true)stopSelf(),或 Activity 发送停止服务的 Intent。

Q5:多浮窗如何管理?

  • 在 Service 中为每个浮窗维护对应的 ViewLayoutParams,并通过接口添加或移除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值