一、项目概述
在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(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
<!-- =================================================================== 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
检查并请求“在其他应用上层显示”权限;
点击“启动”后启动
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
处理拖拽。
以上就是android实现悬浮按钮功能的详细内容,更多关于android悬浮按钮的资料请关注代码网其它相关文章!
发表评论