开发者

Android实现悬浮按钮功能

目录
  • 一、项目概述
  • 二、相关技术知识
  • 三、实现思路
  • 四、整合代码
    • 4.1 Java 代码(MainActivity.java,含两个类)
    • 4.2 XML 与 Manifest
  • 五、代码解读
    • 六、项目总结
      • 七、实践建议与未来展望

        一、项目概述

        在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(Floating Button),用来快速启动工具、展示未读信息或快捷操作。它的特点是:

        • 始终悬浮:在其他应用之上显示,不被当前 Activity 覆盖;

        • 可拖拽:用户可以长按拖动到屏幕任意位置;

        • 点击响应:点击后执行自定义逻辑;

        • 自动适配:适应不同屏幕尺寸和屏幕旋转。

        本项目演示如何使用 android 的 WindowManager + Service + SYSTEM_ALERT_WINDOW 权限,在 Android 8.0+(O)及以上通过 TYPE_APPLICATION_OVERLAY 实现一个可拖拽、可点击的悬浮按钮。

        二、相关技术知识

        1. 悬浮窗权限

          • 从 Android 6.0 开始需用户授予“在其他应用上层显示”权限(ACTION_MANAGE_OVERLAY_PERMISSION);

        2. WindowManager

          • 用于在系统窗口层级中添加自定义 View,LayoutParams 可指定位置、大小、类型等;

        3. Service

          • 利用前台 Service 保证悬浮窗在后台或应用退出后仍能继续显示;

        4. 触摸事件处理

          • 在悬浮 View 的 OnTouchListener 中处理 ACTION_DOWN/ACTION_MOVE 事件,实现拖拽;

        5. 兼容性

          • Android O 及以上需使用 TYPE_APPLICATION_OVERLAY;以下使用 TYPE_PHONE 或 TYPE_SYSTEM_ALERT

        三、实现思路

        1. 申请悬浮窗权限

          • 在 MainActivity 中检测 Settings.canDrawOverlays(),若未授权则跳转系统设置请求;

        2. 创建前台 Service

          • FloatingService 继承 Service,在 onCreate() 时初始化并向 WindowManager 添加悬浮按钮 View;

          • 在 onDestroy() 中移除该 View;

        3. 悬浮 View 布局

          • floating_view.xml 包含一个 ImageView(可替换为任何 View);

          • 设置合适的背景和尺寸;

        4. 拖拽与点击处理

          • 对悬浮按钮设置 OnTouchListener,记录按下时的坐标与初始布局参数,响应移动;

          • 在 ACTION_UP 且位移较小的情况下视为点击,触发自定义逻辑(如 Toast);

        5. 启动与停止 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 on编程客栈Touch(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

        <!-- ==============================================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>

        五、代码解读

        1. MainActivity

          • 检查并请求“在其他应用上层显示”权限;

          • 点击“启动”后启动 FloatingService;点击“停止”后停止 Service。

        2. FloatingService

          • 创建前台通知以提高进程优先级;

          • 使用 WindowManager + TYPE_APPLICATION_OVERLAY(O 及以上)或 TYPE_PHONE(以下),向系统窗口层添加 floating_view

          • 在 OnTouchListener 中处理拖拽与点击:短点击触发 Toast,长拖拽更新 LayoutParams 并调用 updateViewLayout()

        3. 布局与资源

          • floating_view.xml 定义按钮视图;

          • float_bg.xml 定义圆形背景;

          • AndroidManifest.xml 声明必要权限和 Service。

        六、项目总结

        本文介绍了在 Android 8.0+ 环境下,如何通过前台 Service 与 WindowManager 实现一个可拖拽、可点击、始终悬浮在其他应用之上的按钮。核心优势:

        • 系统悬浮窗:不依赖任何 Activity,无论在任何界面都可显示;

        • 灵活拖拽:用户可自由拖动到屏幕任意位置;

        • 点击回调:可在点击时执行自定义逻辑(启动 Activity、切换页面等);

        • 前台 Service:保证在后台也能持续显示,不易被系统回收。

        七、实践建议与未来展望

        1. 美化与动画

          • 为按钮添加 ShadowLayer 或 elevation 提升立体感;

          • 在显示/隐藏时添加淡入淡出动画;

        2. 自定义布局

          • 气泡菜单、多按钮悬浮菜单、可扩展为多种操作;

        3. 权限引导

          • 自定义更友好的权限申请界面,检查失败后提示用户如何开启;

        4. 资源兼容

          • 针对深色模式、自适应布局等场景优化;

        5. Compose 方案

          • 在 Jetpack Compose 中编程客栈可用 AndroidView 或 WindowManager 同样实现,结合 Modifier.pointerInput 处理拖拽。

        以上就是Android实现悬浮按钮功能的详细内容,更多关于Android悬浮按钮的资料请关注编程客栈(www.devze.com)其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜