一、项目介绍
在 Android 应用中,**悬浮窗(Floating Window)**又称系统级别的“悬浮球”或“悬浮视图”,可以在所有应用界面之上悬浮显示,常用于:
-
悬浮工具:音乐控制、悬浮记事、翻译小工具
-
悬浮通知:来电通知、消息提醒
-
悬浮快捷入口:一键拍照、一键拨号
-
辅助功能:屏幕取色、辅助点击
一个完整的浮窗功能,需要解决以下关键点:
-
权限申请:在 Android 6.0+ 需获取
ACTION_MANAGE_OVERLAY_PERMISSION
-
WindowManager:以
TYPE_APPLICATION_OVERLAY
添加悬浮视图 -
View 生命周期:服务(Service)中管理悬浮视图的创建与销毁
-
触摸事件:支持拖拽、点击、缩放等交互
-
布局与样式:自定义布局、支持多种内容
-
性能与节电:保证悬浮视图不影响电量与流畅度
-
兼容性:兼容不同 Android 版本与设备
本文将从原理剖析、实现思路、完整代码示例、代码解读、性能优化、扩展思考及常见问答等方面进行深度阐述,助你快速掌握 Android 悬浮窗开发的全流程方案。
二、相关知识
-
SYSTEM_ALERT_WINDOW 权限
-
Android 6.0(API 23)后需用户手动授予“在其他应用上层显示”权限,跳转到设置页面开启。
-
-
WindowManager.LayoutParams 类型
-
TYPE_APPLICATION_OVERLAY
(API 26+)替代旧的TYPE_PHONE
/TYPE_SYSTEM_ALERT
,需配合SYSTEM_ALERT_WINDOW
。
-
-
WindowManager
-
通过
getSystemService(WINDOW_SERVICE)
获取,使用addView()
、updateViewLayout()
、removeView()
管理悬浮视图。
-
-
Service
-
通常在
Service
中管理悬浮视图,防止被 Activity 生命周期影响;前台 Service 可提高存活优先级。
-
-
触摸拦截与事件分发
-
悬浮视图需要自行处理
onTouchEvent()
或在自定义View
中拦截ACTION_DOWN
/MOVE
/UP
以实现拖拽与点击。
-
-
布局文件
-
可自定义 XML 布局,支持 ImageView、TextView、Button 或任意组合。
-
-
电量与性能优化
-
悬浮视图一般只需在用户交互时绘制,保持轻量;避免在
onDraw()
中做大量运算。
-
三、实现思路
-
界面入口
-
在主
Activity
提供按钮,一键进入悬浮窗权限设置并启动FloatWindowService
。
-
-
权限申请
-
检测
Settings.canDrawOverlays()
,若未开启则调用ACTION_MANAGE_OVERLAY_PERMISSION
。
-
-
服务管理
-
在
FloatWindowService
的onCreate()
中创建并添加悬浮视图,在onDestroy()
中移除。 -
可提升为前台 Service,确保长期存活。
-
-
浮窗视图
-
自定义
FloatWindowView
(可继承FrameLayout
或纯View
),在内部加载布局float_window.xml
。 -
在
onTouchEvent()
中计算偏移量,调用windowManager.updateViewLayout()
更新位置,实现拖拽。 -
处理点击事件,如打开主界面、显示菜单等。
-
-
布局与样式
-
float_window.xml
定义圆形、半透明、带阴影的浮窗样式;支持在布局中放置任意控件。
-
-
资源释放
-
在
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>
六、代码解读
-
权限与启动
-
MainActivity
通过Settings.canDrawOverlays()
检测悬浮窗权限,若未授予则引导至系统设置页; -
授权后启动
FloatWindowService
,并结束Activity
,保持界面整洁。
-
-
前台服务
-
FloatWindowService
在onCreate()
中调用startForeground()
,将服务提升至前台,避免系统回收; -
在
onDestroy()
中移除悬浮视图,防止内存泄漏。
-
-
WindowManager 与 LayoutParams
-
使用
TYPE_APPLICATION_OVERLAY
(Android O+)或兼容旧版本的TYPE_PHONE
; -
FLAG_NOT_FOCUSABLE
保证点击事件只传递给浮窗内部,背后界面仍可交互; -
FLAG_LAYOUT_NO_LIMITS
允许浮窗拖出屏幕边界部分位置。
-
-
自定义视图
FloatWindowView
-
在构造中使用 ViewBinding 加载
float_window_layout.xml
,方便维护与扩展; -
通过
setOnTouchListener
实现拖拽:记录初始按下坐标与悬浮窗初始偏移,计算位移后调用updateViewLayout()
; -
通过
setOnClickListener
实现点击事件。
-
-
布局与样式
-
float_window_layout.xml
定义FrameLayout
容器,内部放置ImageView
; -
float_window_bg.xml
使用<shape>
定义半透明白色圆形背景,配合padding
调整大小。
-
七、性能与优化
-
轻量化
-
浮窗仅承载一个小视图,避免在视图中进行大量更新或动画;
-
-
电量控制
-
若浮窗需要定时刷新,可使用
Handler
并在不可见时暂停;
-
-
优先级管理
-
前台服务保证系统不轻易回收,但也会消耗少量电量,需视场景决定是否使用;
-
-
多浮窗管理
-
若有多个悬浮视图,可在 Service 中维护一组
View
与LayoutParams
,统一管理与更新。
-
八、项目总结与扩展思路
本文从权限管理、WindowManager、Service、自定义视图、交互等方面,完整演示了 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 中为每个浮窗维护对应的
View
与LayoutParams
,并通过接口添加或移除。