Android实现悬浮按钮功能

2025-04-21 05:50

本文主要是介绍Android实现悬浮按钮功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍...

一、项目概述

在很多场景中,我们希望在应用系统任意界面上都能看到一个小的“悬浮按钮”(Floating Button),用来快速启动工具、展示未读信息或快捷操作。它的特点是:

  • 始终悬浮:在其他应用之上显示,不被当前 Activity 覆盖;

  • 可拖拽:用户可以长按拖动到屏幕任意位置;

  • 点击响应:点击后执行自定义逻辑;

  • 自动适配:适应不同屏幕尺寸和屏幕旋转。

本项目演示如何使用 android 的 WindowManager + Service + SYSTEM_ALERT_WINDOW 权限,在 Android 8.0+(O)及以上通过 TYPE_APPLICATION_OVERLAY 实现一个可拖拽、可点击的悬浮按钮。

二、相关技术知识

  1. 悬浮窗权限

    • 从 Android 6.0 开始需用户授予“在其他应用上层显示”权限(ACTION_MANAGE_OVERLAY_PERMISSION);

  2. WindowManager

    • 用于在系统窗口层级中添加自定义 View,LayoutParams 可指定位置、大小、类型等;

  3. Service

    • 利用前台 Service 保证悬浮窗在后台或应用退出后仍能继续显示;

  4. 触摸事件处理

    • 在悬浮 View 的 OnTouchListener 中处理 ACTION_DOWN/ACTION_MOVE 事件,实现拖拽;

  5. 兼容性

    • Android O 及以上需使用 TYPE_APPLICATION_OVERLAY;以下使用 TYPE_PHONE 或 TYPE_SYSTEM_ALERT

三、实现思路

  1. 申请悬浮窗权限

    • 在 MainActivity 中检测 Settings.canDrawOverlays(),若未授权则跳转系统设置请求;

  2. 创建前台 Service

    • FloatingService 继承 Service,在 onCreate() 时初始化并向 WindowManager 添加悬浮按钮 View;

    • 在 onDestroy() 中移除该 View;

  3. 悬浮 View 布局

    • floating_view.xml 包含一个 ImageView(可替换为任何 View);

    • 设置合适的背景和尺寸;

  4. 拖拽与点击处理

    • 对悬浮按钮设置 OnTouchListener,记录按下时的坐标与初始布局参数,响应移动;

    • 在 ACTION_UP 且位移较小的情况下视为点击,触发自定义逻辑(如 Toast);

  5. 启动与停止 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 onChina编程Touch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
 China编程                   // 记录按下时数据
                    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

<!-- ==============================================python=====================
     AndroidManifest.xml — 入口、权限与 Service 声明
=================================================================== -->
<manifest xmlns:android="http://schemas.android.com/apkjs/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>

五、代码解读

  1. MainActivity

    • 检查并请求“在其他应用上层显示”权限;

    • 点击“启动”后启动 FloatingService;点击“停止”后停止 Service。

  2. FloatingService

    • 创建前台通知以提高进程优先级;

    • 使用 WindowManager + TYPE_APPLICATION_OVERLAY(O 及以上)或 TYPE_PHONE(以下),向系统窗口层添加 floating_view

    • 在 OnTouchListener 中处理拖拽与点击:短点击触发 Toast,长拖拽更新 LayoutParams 并调用 updateViewLayout()

  3. 布局与资源

    • floating_view.xml 定义按钮视图;

    • float_bg.xml 定义圆形背景;

    • AndroidManifest.xml 声明必要权限和 Service。

六、项目总结

本文介绍了在 Android 8.0+ 环境下,如何通过前台 Service 与 WindowManager 实现一个可拖拽、可点击、始终悬浮在其他应用之上的按钮。核心优势:

  • 系统悬浮窗:不依赖任何 Activity,无论在任何界面都可显示;

  • 灵活拖拽:用户可自由拖动到屏幕任意位置;

  • 点击回调:可在点击时执行自定义逻辑(启动 Activity、切换页面等);

  • 前台 Service:保证在后台也能持续显示,不易被系统回收。

七、实践建议与未来展望

  1. 美化与动画

    • 为按钮添加 ShadowLayer 或 elevation 提升立体感;

    • 在显示/隐藏时添加淡入淡出动画;

  2. 自定义布局

    • 气泡菜单、多按钮悬浮菜单、可扩展为多种操作;

  3. 权限引导

    • 自定义更友好的权限申请界面,检查失败后提示用户如何开启;

  4. 资源兼容

    • 针对深色模式、自适应布局等场景优化

  5. Compose 方案

    • 在 Jetpack Compose 中China编程可用 AndroidView 或 WindowManager 同样实现,结合 Modifier.pointerInput 处理拖拽。

以上就是Android实现悬浮按钮功能的详细内容,更多关于Android悬浮按钮的资料请关注China编程(www.chinasem.cn)其它相关文章!

这篇关于Android实现悬浮按钮功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154303

相关文章

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

OpenCV实现实时颜色检测的示例

《OpenCV实现实时颜色检测的示例》本文主要介绍了OpenCV实现实时颜色检测的示例,通过HSV色彩空间转换和色调范围判断实现红黄绿蓝颜色检测,包含视频捕捉、区域标记、颜色分析等功能,具有一定的参考... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间

苹果macOS 26 Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色

《苹果macOS26Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色》在整体系统设计方面,macOS26采用了全新的玻璃质感视觉风格,应用于Dock栏、应用图标以及桌面小部件等多个界面... 科技媒体 MACRumors 昨日(6 月 13 日)发布博文,报道称在 macOS 26 Tahoe 中

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

基于Python实现一个Windows Tree命令工具

《基于Python实现一个WindowsTree命令工具》今天想要在Windows平台的CMD命令终端窗口中使用像Linux下的tree命令,打印一下目录结构层级树,然而还真有tree命令,但是发现... 目录引言实现代码使用说明可用选项示例用法功能特点添加到环境变量方法一:创建批处理文件并添加到PATH1

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal

Nexus安装和启动的实现教程

《Nexus安装和启动的实现教程》:本文主要介绍Nexus安装和启动的实现教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、Nexus下载二、Nexus安装和启动三、关闭Nexus总结一、Nexus下载官方下载链接:DownloadWindows系统根

SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程

《SpringBoot集成LiteFlow实现轻量级工作流引擎的详细过程》LiteFlow是一款专注于逻辑驱动流程编排的轻量级框架,它以组件化方式快速构建和执行业务流程,有效解耦复杂业务逻辑,下面给大... 目录一、基础概念1.1 组件(Component)1.2 规则(Rule)1.3 上下文(Conte