Android实现全局悬浮球(附带源码)

【Android开发实战】实现全局悬浮球(超详细讲解+完整代码+原理解析)

下面开始正文:


一、项目介绍

1. 背景与意义

全局悬浮球(Floating Ball)是一种常见的系统级快捷入口,应用场景包括但不限于:

  • 辅助功能:如 AssistiveTouch,为行动不便用户提供触摸快捷键;

  • 快捷操作:悬浮球内可自定义多功能菜单,快速启动应用内部核心功能;

  • 悬浮提示:如悬浮客服、浮动小窗显示实时数据;

  • 游戏辅助:悬浮取图、录屏、快捷按钮等;

实现全局悬浮球,需要突破 App 生命周期限制,将UI悬浮在任意界面之上。要点在于:

  1. 悬浮窗权限管理:Android 6.0+ 需要动态申请 SYSTEM_ALERT_WINDOW 权限;

  2. WindowManager 服务:将自定义 View 添加到系统 WindowManager;

  3. Service 常驻:使用前台服务保持悬浮球存活,避免被系统回收;

  4. 触摸与拖拽:实现对悬浮球的按压、拖动、点击响应;

  5. 吸附边缘算法:在拖动结束后自动吸附到屏幕左/右边缘;

  6. 资源与性能优化:确保悬浮球不会频繁占用 CPU、内存或耗电;

  7. 多进程与适配:处理 Android 8.0+ 不同进程模型以及各家定制 ROM 限制。


二、相关知识详解

1. Android WindowManager 原理

  • WindowManager 是系统级服务,管理所有应用窗口;

  • 使用 Context.getSystemService(WINDOW_SERVICE) 获取;

  • addView(View view, WindowManager.LayoutParams params) 将自定义 View 挂在最顶层。

2. 悬浮窗权限(SYSTEM_ALERT_WINDOW)

  • 在 Android 6.0+ 需打开“允许在其他应用上层显示”;

  • 通过 Intent 跳转 Settings.ACTION_MANAGE_OVERLAY_PERMISSION

  • 在 Android 11+ 权限 UI 与路径可能变化,需兼容提示。

3. Service 与前台服务

  • 将悬浮球逻辑放在 Service 中,保证在 Activity 销毁后仍存活;

  • Android 8.0+ 新增前台服务限制,需调用 startForeground() 并提供通知;

4. WindowManager.LayoutParams 参数详解

  • type:选择 TYPE_APPLICATION_OVERLAY(Android 8.0+)或 TYPE_PHONE(旧版);

  • flagsFLAG_NOT_FOCUSABLE | FLAG_LAYOUT_IN_SCREEN | FLAG_NOT_TOUCH_MODAL

  • format:一般使用 PixelFormat.TRANSLUCENT

  • gravity:控制初始对齐,如 Gravity.START|Gravity.TOP

  • x, y:窗口在屏幕上的坐标。

5. View 拖拽与触摸事件

  • 在悬浮球 View 中重写 onTouchEvent(MotionEvent ev)

  • ACTION_DOWN: 记录初始触点与窗口位置;

  • ACTION_MOVE: 计算增量并调用 updateViewLayout() 更新布局;

  • ACTION_UP: 触发边缘吸附算法。

6. 粘性吸附算法原理

  • 判断悬浮球中心 X 坐标与屏幕宽度中线的距离;

  • 小于中线 → 吸附左侧(x=0);否则吸附右侧(x=屏幕宽度–悬浮球宽度);

  • Y 坐标需约束在 [0, screenHeight–球高度]

  • 使用 ValueAnimator 平滑过渡。

7. 电量与内存优化

  • 避免频繁 postInvalidate()

  • View 渲染简单,使用 ImageView+透明 PNG 而非 Canvas 绘制;

  • Service 调度轻量,拖拽时只更新一次/帧。

8. 权限兼容与适配

  • 小米、华为等定制 ROM 对悬浮窗权限有额外管理页面;

  • 检测 Settings.canDrawOverlays() 并引导到对应权限界面;

  • Android 10+ 没有变化,但需处理 TYPE_APPLICATION_OVERLAY


三、项目实现思路

  1. 编写 FloatingService

    • 继承 Service

    • onCreate() 中初始化悬浮球视图,并调用 addFloatingView()

    • onDestroy() 中移除视图。

  2. 权限申请流程

    • MainActivity 中检测并申请权限;

    • 若权限授予,启动 FloatingService

    • 否则弹出对话框并跳转设置页。

  3. 悬浮球视图布局

    • floating_view.xml:包含一个圆形 ImageView

    • 可选扩展:长按展开菜单 LinearLayoutRecyclerView

  4. WindowManager 添加与更新

    • 创建 WindowManager.LayoutParams params 并配置;

    • wm.addView(floatingView, params)

    • 在拖拽时 params.x = newX; params.y = newY; wm.updateViewLayout(view, params);

  5. 拖拽与吸附

    • View 内处理触摸事件并向 Service 回传更新坐标;

    • 在滑动结束时调用 startAbsorbAnimation(),计算目标 x、y 并用 ValueAnimator 插值更新。

  6. 前台服务通知

    • Android 8.0+ 调用 startForeground(NOTIFY_ID, buildNotification())

    • 通知点击可跳转到 App 主界面或隐藏悬浮球。

  7. 生命周期管理

    • Service 保持悬浮球存活;

    • 在屏幕旋转、分屏、应用升级等场景下,检测并重新添加视图。

  8. 扩展功能

    • 点击悬浮球显示快捷菜单;

    • 长按拖拽、双击隐藏;

    • 定制动画效果:呼吸、旋转。


四、完整整合版代码

// ======================================================
// AndroidManifest.xml
// - 申请悬浮窗权限声明
// - 注册 FloatingService
// ======================================================
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application ...>
    <service android:name=".FloatingService"
             android:foregroundServiceType="mediaProjection|location"/>
</application>

// ======================================================
// res/layout/floating_view.xml
// - 定义悬浮球及可选菜单
// ======================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/float_root"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <!-- 悬浮球 -->
    <ImageView
        android:id="@+id/float_ball"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:src="@drawable/ic_float_ball"
        android:background="@drawable/float_ball_bg"
        android:contentDescription="悬浮球"/>

    <!-- 扩展菜单:默认隐藏 -->
    <LinearLayout
        android:id="@+id/float_menu"
        android:orientation="vertical"
        android:visibility="gone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp">
        <!-- 菜单按钮示例 -->
        <ImageButton android:layout_width="40dp" android:layout_height="40dp"
            android:src="@drawable/ic_action1" android:background="?attr/selectableItemBackgroundBorderless"/>
        <ImageButton android:layout_width="40dp" android:layout_height="40dp"
            android:src="@drawable/ic_action2" android:background="?attr/selectableItemBackgroundBorderless"/>
    </LinearLayout>
</FrameLayout>

// ======================================================
// FloatingService.java
// - 前台 Service,管理悬浮球的添加、移除、拖拽、吸附
// ======================================================
package com.example.floating;

import android.app.*;
import android.content.*;
import android.graphics.PixelFormat;
import android.os.*;
import android.provider.Settings;
import android.view.*;
import android.view.animation.*;
import android.widget.*;

import androidx.core.app.NotificationCompat;

public class FloatingService extends Service {
    private WindowManager wm;
    private View floatView;
    private WindowManager.LayoutParams params;
    private int screenWidth, screenHeight;
    private float downX, downY, viewX, viewY;
    private Handler handler = new Handler(Looper.getMainLooper());

    @Override public void onCreate() {
        super.onCreate();
        wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        screenWidth = dm.widthPixels;
        screenHeight = dm.heightPixels;

        inflateFloatView();
        startForeground(1, buildNotification());
    }

    // 构建前台服务通知
    private Notification buildNotification() {
        NotificationChannel ch = new NotificationChannel("float", "FLOAT", NotificationManager.IMPORTANCE_LOW);
        ((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).createNotificationChannel(ch);
        return new NotificationCompat.Builder(this, "float")
            .setContentTitle("悬浮球运行中")
            .setSmallIcon(R.drawable.ic_float_ball)
            .setOngoing(true)
            .build();
    }

    // 加载并添加悬浮视图
    private void inflateFloatView() {
        floatView = LayoutInflater.from(this).inflate(R.layout.floating_view, null);
        final ImageView ball = floatView.findViewById(R.id.float_ball);
        final LinearLayout menu = floatView.findViewById(R.id.float_menu);

        params = new WindowManager.LayoutParams();
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.height= WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; // Android 8.0+
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                      WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        params.gravity = Gravity.START | Gravity.TOP;
        params.x = 0; params.y = screenHeight / 3;

        wm.addView(floatView, params);

        ball.setOnTouchListener(new View.OnTouchListener() {
            private long touchStart;

            @Override public boolean onTouch(View v, MotionEvent e) {
                switch (e.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        touchStart = System.currentTimeMillis();
                        downX = e.getRawX();
                        downY = e.getRawY();
                        viewX = params.x;
                        viewY = params.y;
                        return true;
                    case MotionEvent.ACTION_MOVE:
                        float deltaX = e.getRawX() - downX;
                        float deltaY = e.getRawY() - downY;
                        params.x = (int)(viewX + deltaX);
                        params.y = (int)(viewY + deltaY);
                        wm.updateViewLayout(floatView, params);
                        return true;
                    case MotionEvent.ACTION_UP:
                        long duration = System.currentTimeMillis() - touchStart;
                        if (duration < 200) {
                            // 点击:切换菜单显示
                            menu.setVisibility(menu.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
                        }
                        absorbEdge();
                        return true;
                }
                return false;
            }
        });

        // 菜单按钮示例点击逻辑
        menu.findViewById(R.id.ic_action1).setOnClickListener(v -> {
            Toast.makeText(this, "Action1", Toast.LENGTH_SHORT).show();
        });
        menu.findViewById(R.id.ic_action2).setOnClickListener(v -> {
            Toast.makeText(this, "Action2", Toast.LENGTH_SHORT).show();
        });
    }

    // 边缘吸附动画
    private void absorbEdge() {
        final int startX = params.x;
        final int endX = (params.x + floatView.getWidth()/2) <= screenWidth/2 ? 0 : screenWidth - floatView.getWidth();
        ValueAnimator anim = ValueAnimator.ofInt(startX, endX);
        anim.setDuration(300);
        anim.setInterpolator(new DecelerateInterpolator());
        anim.addUpdateListener(animation -> {
            params.x = (int) animation.getAnimatedValue();
            wm.updateViewLayout(floatView, params);
        });
        anim.start();
    }

    @Override public void onDestroy() {
        super.onDestroy();
        if (floatView != null) wm.removeView(floatView);
    }

    @Override public IBinder onBind(Intent intent) { return null; }
}

// ======================================================
// MainActivity.java
// - 检测并申请悬浮窗权限,启动 FloatingService
// ======================================================
package com.example.floating;

import android.content.*;
import android.net.Uri;
import android.os.*;
import android.provider.Settings;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private static final int REQ_OVERLAY = 1000;

    @Override protected void onCreate(@Nullable Bundle s) {
        super.onCreate(s);
        setContentView(R.layout.activity_main);
        if (Settings.canDrawOverlays(this)) {
            startService(new Intent(this, FloatingService.class));
        } else {
            new AlertDialog.Builder(this)
                .setTitle("悬浮窗权限")
                .setMessage("请授予悬浮窗权限,否则无法显示悬浮球")
                .setPositiveButton("去授予", (d, w) -> {
                    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + getPackageName()));
                    startActivityForResult(intent, REQ_OVERLAY);
                }).setCancelable(false).show();
        }
    }

    @Override protected void onActivityResult(int req, int res, Intent data) {
        super.onActivityResult(req, res, data);
        if (req == REQ_OVERLAY && Settings.canDrawOverlays(this)) {
            startService(new Intent(this, FloatingService.class));
        } else {
            finish(); // 权限未授予,退出或提示
        }
    }
}

// ======================================================
// res/drawable/float_ball_bg.xml
// - 圆形背景 shape
// ======================================================
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#CC000000"/>
</shape>

// ======================================================
// res/layout/activity_main.xml
// - 主界面布局示例
// ======================================================
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:gravity="center">
    <Button
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="启动悬浮球"
        android:onClick="onStartFloat"/>
</FrameLayout>

五、代码解读

  1. 权限申请

    • 使用 Settings.canDrawOverlays() 判断权限;

    • 通过 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 跳转系统设置页;

    • onActivityResult() 中重新检查并启动 Service。

  2. FloatingService

    • onCreate() 初始化 WindowManager、屏幕尺寸、View;

    • 前台服务:startForeground() 保持存活;

    • inflateFloatView() 加载 floating_view.xml,设置触摸监听;

  3. 触摸与拖拽

    • ACTION_DOWN 记录触点与原始布局位置;

    • ACTION_MOVE 计算偏移并 wm.updateViewLayout()

    • ACTION_UP 判断点击 vs 拖动,并调用 absorbEdge() 吸附;

  4. 吸附边缘

    • 取视图中点与屏幕中线比较,决定吸附左侧或右侧;

    • 使用 ValueAnimator 平滑过渡;

  5. 前台服务通知

    • 避免 Android 8.0+ 被系统回收;

    • 通知点击可扩展为“隐藏悬浮球”或跳转 App;

  6. 资源与样式

    • float_ball_bg.xml 使用纯色圆形 Shape;

    • floating_view.xml 布局简洁,易于扩展;


六、项目总结与拓展

  • 项目收获

    • 深入理解 WindowManager 原理与悬浮窗权限;

    • 掌握 Service 与前台服务保活机制;

    • 熟练实现 View 拖拽、点击与吸附动画;

    • 学会处理 Android 8.0+ 特殊类型 TYPE_APPLICATION_OVERLAY

  • 性能与兼容性

    • 避免在悬浮球中执行耗时操作;

    • 仅在必要时刷新位置信息,降低 CPU 与电量消耗;

    • 对小米、华为等定制系统进行权限适配,并提示用户;

  • 功能拓展

    • 多功能菜单:长按或点击弹出更多快捷操作;

    • 滑出隐藏:在不操作时半隐藏到屏幕侧边;

    • 放大镜:在悬浮球上集成屏幕放大功能;

    • 悬浮工具箱:集成截屏、录屏、手电筒等多种工具;

    • 多进程支持:在多个进程内共享悬浮球状态;

    • 权限插件化:动态请求并兼容系统 ROM。

  • 架构与发布

    • 将悬浮球逻辑提炼为独立模块或 AAR,便于多个项目复用;

    • 对外提供统一 API FloatingManager.show() / hide()

    • 引入 RxJava 或 LiveData 替代 Handler 进行异步事件分发;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值