一、项目概述
在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(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悬浮按钮的资料请关注代码网其它相关文章!
发表评论