本文主要是介绍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实现悬浮按钮功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!