设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间01 现在时间01 缓存数据 当你走完一段之后回头看,你会发现,那些真正能被记得的事真的是没有多少,真正无法忘记的人屈指可数,真正有趣的日子不过是那么一些,而真正需要害怕的也是寥寥无几。

当你走完一段之后回头看,你会发现,那些真正能被记得的事真的是没有多少,真正无法忘记的人屈指可数,真正有趣的日子不过是那么一些,而真正需要害怕的也是寥寥无几。

查看: 151|回复: 1

Android实现悬浮按钮功能

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
32
主题
26
精华
0
金钱
94
积分
58
注册时间
2023-9-29
最后登录
2025-9-8

发表于 2025-9-8 12:53:49 | 显示全部楼层 |阅读模式
一、项目概述

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

  • 始终悬浮:在其他应用之上显示,不被当前 Activity 覆盖;
  • 可拖拽:用户可以长按拖动到屏幕任意位置;
  • 点击响应:点击后执行自定义逻辑;
  • 自动适配:适应不同屏幕尺寸和屏幕旋转。
本项目演示如何使用 Android 的
  1. WindowManager
复制代码
+
  1. Service
复制代码
+
  1. SYSTEM_ALERT_WINDOW
复制代码
权限,在 Android 8.0+(O)及以上通过
  1. TYPE_APPLICATION_OVERLAY
复制代码
实现一个可拖拽、可点击的悬浮按钮。

二、相关技术知识


  • 悬浮窗权限

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

  • WindowManager

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

  • Service

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

  • 触摸事件处理

    • 在悬浮 View 的
      1. OnTouchListener
      复制代码
      中处理
      1. ACTION_DOWN
      复制代码
      /
      1. ACTION_MOVE
      复制代码
      事件,实现拖拽;

  • 兼容性

    • Android O 及以上需使用
      1. TYPE_APPLICATION_OVERLAY
      复制代码
      ;以下使用
      1. TYPE_PHONE
      复制代码
      1. TYPE_SYSTEM_ALERT
      复制代码



三、实现思路


  • 申请悬浮窗权限

      1. MainActivity
      复制代码
      中检测
      1. Settings.canDrawOverlays()
      复制代码
      ,若未授权则跳转系统设置请求;

  • 创建前台 Service

      1. FloatingService
      复制代码
      继承
      1. Service
      复制代码
      ,在
      1. onCreate()
      复制代码
      时初始化并向
      1. WindowManager
      复制代码
      添加悬浮按钮 View;
      1. onDestroy()
      复制代码
      中移除该 View;

  • 悬浮 View 布局

      1. floating_view.xml
      复制代码
      包含一个
      1. ImageView
      复制代码
      (可替换为任何 View);
    • 设置合适的背景和尺寸;

  • 拖拽与点击处理

    • 对悬浮按钮设置
      1. OnTouchListener
      复制代码
      ,记录按下时的坐标与初始布局参数,响应移动;
      1. ACTION_UP
      复制代码
      且位移较小的情况下视为点击,触发自定义逻辑(如
      1. Toast
      复制代码
      );

  • 启动与停止 Service

      1. MainActivity
      复制代码
      的“启动悬浮”按钮点击后启动
      1. FloatingService
      复制代码

    • 在“停止悬浮”按钮点击后停止 Service。


四、整合代码


4.1 Java 代码(MainActivity.java,含两个类)
  1. package com.example.floatingbutton;

  2. import android.app.Notification;
  3. import android.app.NotificationChannel;
  4. import android.app.NotificationManager;
  5. import android.app.PendingIntent;
  6. import android.app.Service;
  7. import android.content.*;
  8. import android.graphics.PixelFormat;
  9. import android.net.Uri;
  10. import android.os.Build;
  11. import android.os.IBinder;
  12. import android.provider.Settings;
  13. import android.view.*;
  14. import android.widget.ImageView;
  15. import android.widget.Toast;
  16. import androidx.annotation.Nullable;
  17. import androidx.appcompat.app.AppCompatActivity;
  18. import android.os.Bundle;
  19. import androidx.core.app.NotificationCompat;

  20. /**
  21. * MainActivity:用于申请权限并启动/停止 FloatingService
  22. */
  23. public class MainActivity extends AppCompatActivity {

  24.     private static final int REQ_OVERLAY = 1000;

  25.     @Override
  26.     protected void onCreate(Bundle savedInstanceState) {
  27.         super.onCreate(savedInstanceState);
  28.         setContentView(R.layout.activity_main);

  29.                 // 启动悬浮按钮
  30.         findViewById(R.id.btn_start).setOnClickListener(v -> {
  31.             if (Settings.canDrawOverlays(this)) {
  32.                 startService(new Intent(this, FloatingService.class));
  33.                 finish(); // 可选:关闭 Activity,悬浮按钮仍会显示
  34.             } else {
  35.                 // 请求悬浮窗权限
  36.                 Intent intent = new Intent(
  37.                   Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
  38.                   Uri.parse("package:" + getPackageName()));
  39.                 startActivityForResult(intent, REQ_OVERLAY);
  40.             }
  41.         });

  42.                 // 停止悬浮按钮
  43.         findViewById(R.id.btn_stop).setOnClickListener(v -> {
  44.             stopService(new Intent(this, FloatingService.class));
  45.         });
  46.     }

  47.     @Override
  48.     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  49.         if (requestCode == REQ_OVERLAY) {
  50.             if (Settings.canDrawOverlays(this)) {
  51.                 startService(new Intent(this, FloatingService.class));
  52.             } else {
  53.                 Toast.makeText(this, "未授予悬浮窗权限", Toast.LENGTH_SHORT).show();
  54.             }
  55.         }
  56.     }
  57. }

  58. /**
  59. * FloatingService:前台 Service,添加可拖拽悬浮按钮
  60. */
  61. public class FloatingService extends Service {

  62.     private WindowManager windowManager;
  63.     private View floatView;
  64.     private WindowManager.LayoutParams params;

  65.     @Override
  66.     public void onCreate() {
  67.         super.onCreate();
  68.         // 1. 创建前台通知
  69.         String channelId = createNotificationChannel();
  70.         Notification notification = new NotificationCompat.Builder(this, channelId)
  71.             .setContentTitle("Floating Button")
  72.             .setContentText("悬浮按钮已启动")
  73.             .setSmallIcon(R.drawable.ic_floating)
  74.             .setOngoing(true)
  75.             .build();
  76.         startForeground(1, notification);

  77.         // 2. 初始化 WindowManager 与 LayoutParams
  78.         windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
  79.         params = new WindowManager.LayoutParams();
  80.         params.width  = WindowManager.LayoutParams.WRAP_CONTENT;
  81.         params.height = WindowManager.LayoutParams.WRAP_CONTENT;
  82.         params.format = PixelFormat.TRANSLUCENT;
  83.         params.flags  = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  84.                       | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
  85.         // 不同 SDK 对悬浮类型的支持
  86.         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  87.             params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
  88.         } else {
  89.             params.type = WindowManager.LayoutParams.TYPE_PHONE;
  90.         }
  91.         // 默认初始位置
  92.         params.gravity = Gravity.TOP | Gravity.START;
  93.         params.x = 100;
  94.         params.y = 300;

  95.         // 3. 载入自定义布局
  96.         floatView = LayoutInflater.from(this)
  97.                       .inflate(R.layout.floating_view, null);
  98.         ImageView iv = floatView.findViewById(R.id.iv_float);
  99.         iv.setOnTouchListener(new FloatingOnTouchListener());

  100.         // 4. 添加到窗口
  101.         windowManager.addView(floatView, params);
  102.     }

  103.     // 前台通知 Channel
  104.     private String createNotificationChannel() {
  105.         String channelId = "floating_service";
  106.         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  107.             NotificationChannel chan = new NotificationChannel(
  108.                 channelId, "悬浮按钮服务",
  109.                 NotificationManager.IMPORTANCE_NONE);
  110.             ((NotificationManager)getSystemService(NOTIFICATION_SERVICE))
  111.                 .createNotificationChannel(chan);
  112.         }
  113.         return channelId;
  114.     }

  115.     @Override
  116.     public void onDestroy() {
  117.         super.onDestroy();
  118.         if (floatView != null) {
  119.             windowManager.removeView(floatView);
  120.             floatView = null;
  121.         }
  122.     }

  123.     @Nullable @Override
  124.     public IBinder onBind(Intent intent) {
  125.         return null;
  126.     }

  127.     /**
  128.      * 触摸监听:支持拖拽与点击
  129.      */
  130.     private class FloatingOnTouchListener implements View.OnTouchListener {
  131.         private int initialX, initialY;
  132.         private float initialTouchX, initialTouchY;
  133.         private long touchStartTime;

  134.         @Override
  135.         public boolean onTouch(View v, MotionEvent event) {
  136.             switch (event.getAction()) {
  137.                 case MotionEvent.ACTION_DOWN:
  138.                     // 记录按下时数据
  139.                     initialX = params.x;
  140.                     initialY = params.y;
  141.                     initialTouchX = event.getRawX();
  142.                     initialTouchY = event.getRawY();
  143.                     touchStartTime = System.currentTimeMillis();
  144.                     return true;
  145.                 case MotionEvent.ACTION_MOVE:
  146.                     // 更新悬浮位置
  147.                     params.x = initialX + (int)(event.getRawX() - initialTouchX);
  148.                     params.y = initialY + (int)(event.getRawY() - initialTouchY);
  149.                     windowManager.updateViewLayout(floatView, params);
  150.                     return true;
  151.                 case MotionEvent.ACTION_UP:
  152.                     long clickDuration = System.currentTimeMillis() - touchStartTime;
  153.                     // 如果按下和抬起位置变化不大且时间短,则视为点击
  154.                     if (clickDuration < 200
  155.                         && Math.hypot(event.getRawX() - initialTouchX,
  156.                                       event.getRawY() - initialTouchY) < 10) {
  157.                         Toast.makeText(FloatingService.this,
  158.                             "悬浮按钮被点击!", Toast.LENGTH_SHORT).show();
  159.                         // 这里可启动 Activity 或其他操作
  160.                     }
  161.                     return true;
  162.             }
  163.             return false;
  164.         }
  165.     }
  166. }
复制代码
4.2 XML 与 Manifest
  1. <!-- ===================================================================
  2.      AndroidManifest.xml — 入口、权限与 Service 声明
  3. =================================================================== -->
  4. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  5.     package="com.example.floatingbutton">
  6.     <!-- 悬浮窗权限 -->
  7.     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  8.     <application ...>
  9.         <activity android:name=".MainActivity">
  10.             <intent-filter>
  11.                 <action android:name="android.intent.action.MAIN"/>
  12.                 <category android:name="android.intent.category.LAUNCHER"/>
  13.             </intent-filter>
  14.         </activity>
  15.         <!-- 声明 Service -->
  16.         <service android:name=".FloatingService"
  17.                  android:exported="false"/>
  18.     </application>
  19. </manifest>
复制代码
  1. <!-- ===================================================================
  2.      activity_main.xml — 包含启动/停止按钮
  3. =================================================================== -->
  4. <?xml version="1.0" encoding="utf-8"?>
  5. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  6.     android:id="@+id/layout_root"
  7.     android:layout_width="match_parent"
  8.     android:layout_height="match_parent"
  9.     android:orientation="vertical"
  10.     android:gravity="center"
  11.     android:padding="24dp">

  12.     <Button
  13.         android:id="@+id/btn_start"
  14.         android:layout_width="wrap_content"
  15.         android:layout_height="wrap_content"
  16.         android:text="启动悬浮按钮"/>

  17.     <Button
  18.         android:id="@+id/btn_stop"
  19.         android:layout_width="wrap_content"
  20.         android:layout_height="wrap_content"
  21.         android:text="停止悬浮按钮"
  22.         android:layout_marginTop="16dp"/>
  23. </LinearLayout>
复制代码
  1. <!-- ===================================================================
  2.      floating_view.xml — 悬浮按钮布局
  3. =================================================================== -->
  4. <?xml version="1.0" encoding="utf-8"?>
  5. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  6.     android:layout_width="48dp"
  7.     android:layout_height="48dp">

  8.     <ImageView
  9.         android:id="@+id/iv_float"
  10.         android:layout_width="match_parent"
  11.         android:layout_height="match_parent"
  12.         android:src="@drawable/ic_float"
  13.         android:background="@drawable/float_bg"
  14.         android:padding="8dp"/>
  15. </FrameLayout>
复制代码
  1. <!-- ===================================================================
  2.      float_bg.xml — 按钮背景(圆形 + 阴影)
  3. =================================================================== -->
  4. <shape xmlns:android="http://schemas.android.com/apk/res/android"
  5.     android:shape="oval">
  6.     <solid android:color="#FFFFFF"/>
  7.     <size android:width="48dp" android:height="48dp"/>
  8.     <corners android:radius="24dp"/>
  9.     <padding android:all="4dp"/>
  10.     <stroke android:width="1dp" android:color="#CCCCCC"/>
  11.     <!-- 阴影需在代码中或 ShadowLayer 中设置 -->
  12. </shape>
复制代码
五、代码解读


  • MainActivity

    • 检查并请求“在其他应用上层显示”权限;
    • 点击“启动”后启动
      1. FloatingService
      复制代码
      ;点击“停止”后停止 Service。

  • FloatingService

    • 创建前台通知以提高进程优先级;
    • 使用
      1. WindowManager
      复制代码
      +
      1. TYPE_APPLICATION_OVERLAY
      复制代码
      (O 及以上)或
      1. TYPE_PHONE
      复制代码
      (以下),向系统窗口层添加
      1. floating_view
      复制代码

      1. OnTouchListener
      复制代码
      中处理拖拽与点击:短点击触发
      1. Toast
      复制代码
      ,长拖拽更新
      1. LayoutParams
      复制代码
      并调用
      1. updateViewLayout()
      复制代码


  • 布局与资源

      1. floating_view.xml
      复制代码
      定义按钮视图;
      1. float_bg.xml
      复制代码
      定义圆形背景;
      1. AndroidManifest.xml
      复制代码
      声明必要权限和 Service。


六、项目总结

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

  • 系统悬浮窗:不依赖任何 Activity,无论在任何界面都可显示;
  • 灵活拖拽:用户可自由拖动到屏幕任意位置;
  • 点击回调:可在点击时执行自定义逻辑(启动 Activity、切换页面等);
  • 前台 Service:保证在后台也能持续显示,不易被系统回收。

七、实践建议与未来展望


  • 美化与动画

    • 为按钮添加
      1. ShadowLayer
      复制代码
      1. elevation
      复制代码
      提升立体感;
    • 在显示/隐藏时添加淡入淡出动画;

  • 自定义布局

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

  • 权限引导

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

  • 资源兼容

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

  • Compose 方案

    • 在 Jetpack Compose 中可用
      1. AndroidView
      复制代码
      1. WindowManager
      复制代码
      同样实现,结合
      1. Modifier.pointerInput
      复制代码
      处理拖拽。

以上就是Android实现悬浮按钮功能的详细内容,更多关于Android悬浮按钮的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
15
积分
10
注册时间
2022-12-26
最后登录
2022-12-26

发表于 2025-11-15 09:57:30 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表