一、项目概述
在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(Floating Button),用来快速启动工具、展示未读信息或快捷操作。它的特点是:
- 始终悬浮:在其他应用之上显示,不被当前 Activity 覆盖;
- 可拖拽:用户可以长按拖动到屏幕任意位置;
- 点击响应:点击后执行自定义逻辑;
- 自动适配:适应不同屏幕尺寸和屏幕旋转。
本项目演示如何使用 Android 的 + + 权限,在 Android 8.0+(O)及以上通过 实现一个可拖拽、可点击的悬浮按钮。
二、相关技术知识
- 悬浮窗权限
- 从 Android 6.0 开始需用户授予“在其他应用上层显示”权限(
- ACTION_MANAGE_OVERLAY_PERMISSION
复制代码 );
- WindowManager
- 用于在系统窗口层级中添加自定义 View,可指定位置、大小、类型等;
- Service
- 利用前台保证悬浮窗在后台或应用退出后仍能继续显示;
- 触摸事件处理
- 兼容性
三、实现思路
- 申请悬浮窗权限
- 在中检测
- Settings.canDrawOverlays()
复制代码 ,若未授权则跳转系统设置请求;
- 创建前台 Service
- 继承,在时初始化并向添加悬浮按钮 View;
- 在中移除该 View;
- 悬浮 View 布局
- 包含一个(可替换为任何 View);
- 设置合适的背景和尺寸;
- 拖拽与点击处理
- 对悬浮按钮设置,记录按下时的坐标与初始布局参数,响应移动;
- 在且位移较小的情况下视为点击,触发自定义逻辑(如);
- 启动与停止 Service
- 在的“启动悬浮”按钮点击后启动;
- 在“停止悬浮”按钮点击后停止 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
- <!-- ===================================================================
- AndroidManifest.xml — 入口、权限与 Service 声明
- =================================================================== -->
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.floatingbutton">
- <!-- 悬浮窗权限 -->
- <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- <application ...>
- <activity android:name=".MainActivity">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
- <!-- 声明 Service -->
- <service android:name=".FloatingService"
- android:exported="false"/>
- </application>
- </manifest>
复制代码- <!-- ===================================================================
- activity_main.xml — 包含启动/停止按钮
- =================================================================== -->
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/layout_root"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:gravity="center"
- android:padding="24dp">
-
- <Button
- android:id="@+id/btn_start"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="启动悬浮按钮"/>
-
- <Button
- android:id="@+id/btn_stop"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="停止悬浮按钮"
- android:layout_marginTop="16dp"/>
- </LinearLayout>
复制代码- <!-- ===================================================================
- floating_view.xml — 悬浮按钮布局
- =================================================================== -->
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="48dp"
- android:layout_height="48dp">
-
- <ImageView
- 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"/>
- </FrameLayout>
复制代码- <!-- ===================================================================
- float_bg.xml — 按钮背景(圆形 + 阴影)
- =================================================================== -->
- <shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="oval">
- <solid android:color="#FFFFFF"/>
- <size android:width="48dp" android:height="48dp"/>
- <corners android:radius="24dp"/>
- <padding android:all="4dp"/>
- <stroke android:width="1dp" android:color="#CCCCCC"/>
- <!-- 阴影需在代码中或 ShadowLayer 中设置 -->
- </shape>
复制代码 五、代码解读
- MainActivity
- 检查并请求“在其他应用上层显示”权限;
- 点击“启动”后启动;点击“停止”后停止 Service。
- FloatingService
- 创建前台通知以提高进程优先级;
- 使用+(O 及以上)或(以下),向系统窗口层添加;
- 在中处理拖拽与点击:短点击触发,长拖拽更新并调用。
- 布局与资源
- 定义按钮视图;
- 定义圆形背景;
- 声明必要权限和 Service。
六、项目总结
本文介绍了在 Android 8.0+ 环境下,如何通过前台 与 实现一个 可拖拽、可点击、始终悬浮在其他应用之上的按钮。核心优势:
- 系统悬浮窗:不依赖任何 Activity,无论在任何界面都可显示;
- 灵活拖拽:用户可自由拖动到屏幕任意位置;
- 点击回调:可在点击时执行自定义逻辑(启动 Activity、切换页面等);
- 前台 Service:保证在后台也能持续显示,不易被系统回收。
七、实践建议与未来展望
- 美化与动画
- 为按钮添加或提升立体感;
- 在显示/隐藏时添加淡入淡出动画;
- 自定义布局
- 权限引导
- 自定义更友好的权限申请界面,检查失败后提示用户如何开启;
- 资源兼容
- Compose 方案
- 在 Jetpack Compose 中可用或同样实现,结合处理拖拽。
以上就是Android实现悬浮按钮功能的详细内容,更多关于Android悬浮按钮的资料请关注晓枫资讯其它相关文章!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |