Android实现悬浮按钮(附带源码)

分类: bet28365365体育投注 时间: 2025-12-20 09:05:05 作者: admin 阅读: 3943 点赞: 829
Android实现悬浮按钮(附带源码)

一、项目概述

在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(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

package="com.example.floatingbutton">

android:exported="false"/>

android:id="@+id/layout_root"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:gravity="center"

android:padding="24dp">

android:id="@+id/btn_start"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="启动悬浮按钮"/>

android:id="@+id/btn_stop"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="停止悬浮按钮"

android:layout_marginTop="16dp"/>

android:layout_width="48dp"

android:layout_height="48dp">

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"/>

android:shape="oval">

五、代码解读

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 处理拖拽。

八、知识拓展与参考资料

WindowManager.LayoutParams 源码解析

SYSTEM_ALERT_WINDOW 权限详解

前台 Service 使用指南

GitHub 示例:搜索 “Android ChatHead Service”

相关推荐