You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需SYSTEM_ALERT_WINDOW权限的Android可拖拽悬浮视图问题

解决方案:无需SYSTEM_ALERT_WINDOW权限实现跨Activity常驻可拖拽悬浮窗

你的核心问题是悬浮窗绑定了单个Activity的Window,导致Activity切换时旧窗口销毁,悬浮窗随之消失。下面是无需系统权限、支持跨Activity常驻且保留交互能力的实现方案:

核心思路

使用Application级别的WindowManager(而非Activity绑定的实例)管理悬浮窗,同时监听Activity生命周期,确保切换时悬浮窗不会被意外移除。

1. 全局初始化悬浮窗组件

在自定义Application类中持有悬浮窗实例和全局WindowManager,避免绑定单个Activity:

public class MyApp extends Application {
    private WindowManager windowManager;
    private FloatingView floatingView;
    private WindowManager.LayoutParams layoutParams;
    private boolean isFloatingEnabled = false; // 标记悬浮窗是否开启

    @Override
    public void onCreate() {
        super.onCreate();
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        initFloatingView();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityStarted(@NonNull Activity activity) {
                // 每次Activity启动时,检查悬浮窗是否存在,不存在则重新添加
                if (isFloatingEnabled && floatingView.getParent() == null) {
                    windowManager.addView(floatingView, layoutParams);
                }
            }

            // 其余生命周期方法留空即可
            @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
            @Override public void onActivityResumed(@NonNull Activity activity) {}
            @Override public void onActivityPaused(@NonNull Activity activity) {}
            @Override public void onActivityStopped(@NonNull Activity activity) {}
            @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
            @Override public void onActivityDestroyed(@NonNull Activity activity) {}
        });
    }

    private void initFloatingView() {
        floatingView = new FloatingView(this);
        // 使用无需权限的窗口类型,仅在当前应用内可见
        layoutParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG,
                // 允许下层Activity接收触摸事件
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT
        );
        layoutParams.gravity = Gravity.TOP | Gravity.START;
        layoutParams.x = 100;
        layoutParams.y = 100;
    }

    // 对外提供开启/关闭悬浮窗的方法
    public void toggleFloatingView() {
        isFloatingEnabled = !isFloatingEnabled;
        if (isFloatingEnabled) {
            showFloatingView();
        } else {
            hideFloatingView();
        }
    }

    private void showFloatingView() {
        if (floatingView.getParent() == null) {
            windowManager.addView(floatingView, layoutParams);
        }
    }

    private void hideFloatingView() {
        if (floatingView.getParent() != null) {
            windowManager.removeView(floatingView);
        }
    }

    // 供悬浮窗拖拽时更新位置使用
    public WindowManager.LayoutParams getLayoutParams() {
        return layoutParams;
    }
}

2. 实现可拖拽的悬浮窗View

自定义包含拖拽逻辑和关闭按钮的悬浮View:

public class FloatingView extends FrameLayout {
    private float lastTouchX, lastTouchY;
    private float initialPosX, initialPosY;
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;

    public FloatingView(Context context) {
        super(context);
        windowManager = (WindowManager) context.getSystemService(WINDOW_SERVICE);
        initView();
    }

    private void initView() {
        // 加载自定义悬浮窗布局(需包含关闭按钮)
        inflate(getContext(), R.layout.layout_floating_window, this);

        // 关闭按钮点击事件
        findViewById(R.id.btn_close).setOnClickListener(v -> {
            ((MyApp) getContext().getApplicationContext()).toggleFloatingView();
        });

        // 拖拽逻辑
        setOnTouchListener((v, event) -> {
            layoutParams = ((MyApp) getContext().getApplicationContext()).getLayoutParams();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastTouchX = event.getRawX();
                    lastTouchY = event.getRawY();
                    initialPosX = layoutParams.x;
                    initialPosY = layoutParams.y;
                    return true;
                case MotionEvent.ACTION_MOVE:
                    float dx = event.getRawX() - lastTouchX;
                    float dy = event.getRawY() - lastTouchY;
                    layoutParams.x = (int) (initialPosX + dx);
                    layoutParams.y = (int) (initialPosY + dy);
                    windowManager.updateViewLayout(this, layoutParams);
                    return true;
            }
            return false;
        });
    }
}

3. 关键注意事项

  • 窗口类型选择:TYPE_APPLICATION_ATTACHED_DIALOG无需申请SYSTEM_ALERT_WINDOW权限,且仅在当前应用内显示,完全符合需求。
  • 触摸事件配置:FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL确保悬浮窗不会拦截下层Activity的触摸交互。
  • 内存泄漏防范:在应用退出时,务必调用hideFloatingView()移除悬浮窗,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Shweta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 14:05:23