本文主要是介绍Android实现悬浮按钮功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍...
一、项目概述
在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(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 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>五、代码解读
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 中China编程可用
AndroidView或WindowManager同样实现,结合Modifier.pointerInput处理拖拽。
以上就是Android实现悬浮按钮功能的详细内容,更多关于Android悬浮按钮的资料请关注China编程(www.chinasem.cn)其它相关文章!
这篇关于Android实现悬浮按钮功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!