无需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
相关产品推荐
相关产品推荐

