Android实现悬浮按钮(附带源码)
一、项目概述
在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(Floating Button),用来快速启动工具、展示未读信息或快捷操作。它的特点是:
始终悬浮:在其他应用之上显示,不被当前 Activity 覆盖;
可拖拽:用户可以长按拖动到屏幕任意位置;
点击响应:点击后执行自定义逻辑;
自动适配:适应不同屏幕尺寸和屏幕旋转。
本项目演示如何使用 Android 的 WindowManager + Service + SYSTEM_ALERT_WINDOW 权限,在 Android 8.0+(O)及以上通过 TYPE_APPLICATION_OVERLAY 实现一个可拖拽、可点击的悬浮按钮。
二、相关技术知识
悬浮窗权限
从 Android 6.0 开始需用户授予“在其他应用上层显示”权限(ACTION_MANAGE_OVERLAY_PERMISSION);
WindowManager
用于在系统窗口层级中添加自定义 View,LayoutParams 可指定位置、大小、类型等;
Service
利用前台 Service 保证悬浮窗在后台或应用退出后仍能继续显示;
触摸事件处理
在悬浮 View 的 OnTouchListener 中处理 ACTION_DOWN/ACTION_MOVE 事件,实现拖拽;
兼容性
Android O 及以上需使用 TYPE_APPLICATION_OVERLAY;以下使用 TYPE_PHONE 或 TYPE_SYSTEM_ALERT。
三、实现思路
申请悬浮窗权限
在 MainActivity 中检测 Settings.canDrawOverlays(),若未授权则跳转系统设置请求;
创建前台 Service
FloatingService 继承 Service,在 onCreate() 时初始化并向 WindowManager 添加悬浮按钮 View;
在 onDestroy() 中移除该 View;
悬浮 View 布局
floating_view.xml 包含一个 ImageView(可替换为任何 View);
设置合适的背景和尺寸;
拖拽与点击处理
对悬浮按钮设置 OnTouchListener,记录按下时的坐标与初始布局参数,响应移动;
在 ACTION_UP 且位移较小的情况下视为点击,触发自定义逻辑(如 Toast);
启动与停止 Service
在 MainActivity 的“启动悬浮”按钮点击后启动 FloatingService;
在“停止悬浮”按钮点击后停止 Service。
四、整合代码
4.1 Java 代码(MainActivity.java,含两个类)
package com.example.floatingbutton;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.*;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.provider.Settings;
import android.view.*;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
/**
* MainActivity:用于申请权限并启动/停止 FloatingService
*/
public class MainActivity extends AppCompatActivity {
private static final int REQ_OVERLAY = 1000;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 启动悬浮按钮
findViewById(R.id.btn_start).setOnClickListener(v -> {
if (Settings.canDrawOverlays(this)) {
startService(new Intent(this, FloatingService.class));
finish(); // 可选:关闭 Activity,悬浮按钮仍会显示
} else {
// 请求悬浮窗权限
Intent intent = new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQ_OVERLAY);
}
});
// 停止悬浮按钮
findViewById(R.id.btn_stop).setOnClickListener(v -> {
stopService(new Intent(this, FloatingService.class));
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_OVERLAY) {
if (Settings.canDrawOverlays(this)) {
startService(new Intent(this, FloatingService.class));
} else {
Toast.makeText(this, "未授予悬浮窗权限", Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* FloatingService:前台 Service,添加可拖拽悬浮按钮
*/
public class FloatingService extends Service {
private WindowManager windowManager;
private View floatView;
private WindowManager.LayoutParams params;
@Override
public void onCreate() {
super.onCreate();
// 1. 创建前台通知
String channelId = createNotificationChannel();
Notification notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("Floating Button")
.setContentText("悬浮按钮已启动")
.setSmallIcon(R.drawable.ic_floating)
.setOngoing(true)
.build();
startForeground(1, notification);
// 2. 初始化 WindowManager 与 LayoutParams
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
// 不同 SDK 对悬浮类型的支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_PHONE;
}
// 默认初始位置
params.gravity = Gravity.TOP | Gravity.START;
params.x = 100;
params.y = 300;
// 3. 载入自定义布局
floatView = LayoutInflater.from(this)
.inflate(R.layout.floating_view, null);
ImageView iv = floatView.findViewById(R.id.iv_float);
iv.setOnTouchListener(new FloatingOnTouchListener());
// 4. 添加到窗口
windowManager.addView(floatView, params);
}
// 前台通知 Channel
private String createNotificationChannel() {
String channelId = "floating_service";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(
channelId, "悬浮按钮服务",
NotificationManager.IMPORTANCE_NONE);
((NotificationManager)getSystemService(NOTIFICATION_SERVICE))
.createNotificationChannel(chan);
}
return channelId;
}
@Override
public void onDestroy() {
super.onDestroy();
if (floatView != null) {
windowManager.removeView(floatView);
floatView = null;
}
}
@Nullable @Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* 触摸监听:支持拖拽与点击
*/
private class FloatingOnTouchListener implements View.OnTouchListener {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
private long touchStartTime;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录按下时数据
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
touchStartTime = System.currentTimeMillis();
return true;
case MotionEvent.ACTION_MOVE:
// 更新悬浮位置
params.x = initialX + (int)(event.getRawX() - initialTouchX);
params.y = initialY + (int)(event.getRawY() - initialTouchY);
windowManager.updateViewLayout(floatView, params);
return true;
case MotionEvent.ACTION_UP:
long clickDuration = System.currentTimeMillis() - touchStartTime;
// 如果按下和抬起位置变化不大且时间短,则视为点击
if (clickDuration < 200
&& Math.hypot(event.getRawX() - initialTouchX,
event.getRawY() - initialTouchY) < 10) {
Toast.makeText(FloatingService.this,
"悬浮按钮被点击!", Toast.LENGTH_SHORT).show();
// 这里可启动 Activity 或其他操作
}
return true;
}
return false;
}
}
}
4.2 XML 与 Manifest
package="com.example.floatingbutton"> android:exported="false"/>
android:id="@+id/layout_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp">
android:layout_width="48dp" android:layout_height="48dp"> android:id="@+id/iv_float" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/ic_float" android:background="@drawable/float_bg" android:padding="8dp"/>
android:shape="oval">
五、代码解读
MainActivity
检查并请求“在其他应用上层显示”权限;
点击“启动”后启动 FloatingService;点击“停止”后停止 Service。
FloatingService
创建前台通知以提高进程优先级;
使用 WindowManager + TYPE_APPLICATION_OVERLAY(O 及以上)或 TYPE_PHONE(以下),向系统窗口层添加 floating_view;
在 OnTouchListener 中处理拖拽与点击:短点击触发 Toast,长拖拽更新 LayoutParams 并调用 updateViewLayout()。
布局与资源
floating_view.xml 定义按钮视图;
float_bg.xml 定义圆形背景;
AndroidManifest.xml 声明必要权限和 Service。
六、项目总结
本文介绍了在 Android 8.0+ 环境下,如何通过前台 Service 与 WindowManager 实现一个可拖拽、可点击、始终悬浮在其他应用之上的按钮。核心优势:
系统悬浮窗:不依赖任何 Activity,无论在任何界面都可显示;
灵活拖拽:用户可自由拖动到屏幕任意位置;
点击回调:可在点击时执行自定义逻辑(启动 Activity、切换页面等);
前台 Service:保证在后台也能持续显示,不易被系统回收。
七、实践建议与未来展望
美化与动画
为按钮添加 ShadowLayer 或 elevation 提升立体感;
在显示/隐藏时添加淡入淡出动画;
自定义布局
气泡菜单、多按钮悬浮菜单、可扩展为多种操作;
权限引导
自定义更友好的权限申请界面,检查失败后提示用户如何开启;
资源兼容
针对深色模式、自适应布局等场景优化;
Compose 方案
在 Jetpack Compose 中可用 AndroidView 或 WindowManager 同样实现,结合 Modifier.pointerInput 处理拖拽。
八、知识拓展与参考资料
WindowManager.LayoutParams 源码解析
SYSTEM_ALERT_WINDOW 权限详解
前台 Service 使用指南
GitHub 示例:搜索 “Android ChatHead Service”