当前位置: 代码网 > it编程>App开发>Android > Android实现悬浮按钮功能

Android实现悬浮按钮功能

2025年04月20日 Android 我要评论
一、项目概述在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(floating button),用来快速启动工具、展示未读信息或快捷操作。它的特点是:始

一、项目概述

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

五、代码解读

  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悬浮按钮的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com