寫一個懸浮在activity上的dialog,在dialog沒有覆蓋的地方,不影響activity的使用
效果圖:

一、權限配置(AndroidManifest.xml)
首先添加懸浮窗必需權限,適配 Android 6.0 + 動態(tài)權限機制:
<!-- 懸浮窗核心權限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- Android 10+ 額外需要(允許在其他應用上層顯示) -->
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
二、懸浮 Dialog 工具類(FloatingDialogHelper.java)
import android.app.Dialog; import android.content.Context; import android.os.Build; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; /** * 懸浮Dialog工具類:支持懸浮在Activity上層,不攔截底層觸摸操作 * 核心特性:兼容高版本、支持全參數自定義、無內存泄漏風險 */ public class FloatingDialogHelper { /** * 創(chuàng)建懸浮Dialog(核心方法) * @param context 上下文(建議使用Activity Context,保證樣式一致性) * @param layoutId Dialog自定義布局ID * @param x 水平偏移量(px,基于左上角定位) * @param y 垂直偏移量(px,基于左上角定位) * @param width Dialog寬度(px,建議用dp轉px適配) * @param height Dialog高度(px,建議用dp轉px適配) * @param alpha 透明度(0.0-1.0,0.0完全透明,1.0不透明) * @param cancelable 點擊返回鍵是否取消Dialog * @param cancelOutside 點擊Dialog外部是否取消 * @return 配置完成的Dialog實例 */ public static Dialog createFloatingDialog(Context context, int layoutId, int x, int y, int width, int height, float alpha, boolean cancelable, boolean cancelOutside) { // 初始化Dialog(傳入Activity Context避免內存泄漏) Dialog dialog = new Dialog(context); // 設置自定義布局 dialog.setContentView(layoutId); // 基礎行為配置 dialog.setCancelable(cancelable); dialog.setCanceledOnTouchOutside(cancelOutside); // 獲取Window對象(核心配置入口) Window window = dialog.getWindow(); if (window == null) return dialog; WindowManager.LayoutParams params = window.getAttributes(); // 1. 適配高版本Window類型(關鍵!避免8.0+崩潰) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Android 8.0+ 推薦使用TYPE_APPLICATION_OVERLAY params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { // 低版本兼容:TYPE_PHONE(需SYSTEM_ALERT_WINDOW權限) params.type = WindowManager.LayoutParams.TYPE_PHONE; } // 2. 位置與尺寸配置 params.gravity = Gravity.LEFT | Gravity.TOP; // 定位基準:左上角 params.x = x; // 水平偏移 params.y = y; // 垂直偏移 params.width = width; // 寬度 params.height = height; // 高度 // 3. 視覺效果配置 params.alpha = alpha; // 整體透明度 params.dimAmount = 0.0f; // 背景遮罩透明度(0.0無遮罩,不影響底層操作) // 4. 觸摸事件配置(關鍵!不攔截底層Activity觸摸) window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); // 5. 應用所有配置 window.setAttributes(params); window.setGravity(Gravity.LEFT | Gravity.TOP); return dialog; } /** * 檢查是否擁有懸浮窗權限(Android 6.0+必需) * @param context 上下文 * @return true:已授權;false:未授權 */ public static boolean hasOverlayPermission(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 6.0+ 需通過Settings檢查權限 return android.provider.Settings.canDrawOverlays(context); } // 低版本默認授予權限 return true; } }
三、Activity 實現(DialogOneActivity.java)
import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; // 替換為你的項目包名(根據實際項目修改) import com.example.yourpackage.databinding.ActivityDialogoneBinding; import com.example.yourpackage.databinding.DialogLayoutBinding; public class DialogOneActivity extends AppCompatActivity { private ActivityDialogoneBinding activityBinding; // Activity布局Binding private DialogLayoutBinding dialogBinding; // Dialog布局Binding(操作Dialog內控件用) private Dialog floatingDialog; // 懸浮Dialog實例 private int clickCount = 1; // 點擊計數(示例邏輯) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化ViewBinding(替代setContentView和findViewById) activityBinding = ActivityDialogoneBinding.inflate(getLayoutInflater()); setContentView(activityBinding.getRoot()); // 初始化按鈕點擊事件(Lambda簡化代碼) activityBinding.btDialog.setOnClickListener(v -> showFloatingDialog()); activityBinding.btDialog1.setOnClickListener(v -> updateClickCount()); } /** * 顯示懸浮Dialog(先檢查權限,再創(chuàng)建顯示) */ private void showFloatingDialog() { // 1. 權限檢查:未授權則引導用戶開啟 if (!FloatingDialogHelper.hasOverlayPermission(this)) { Toast.makeText(this, "請開啟懸浮窗權限以顯示Dialog", Toast.LENGTH_SHORT).show(); jumpToOverlaySetting(); // 跳轉到權限設置頁 return; } // 2. 避免重復顯示:銷毀已有Dialog if (floatingDialog != null && floatingDialog.isShowing()) { floatingDialog.dismiss(); } // 3. 配置Dialog參數(可根據業(yè)務動態(tài)調整) int x = dp2px(100); // 水平偏移:100dp轉px(適配不同屏幕) int y = dp2px(100); // 垂直偏移:100dp轉px int width = dp2px(350); // 寬度:350dp(原700px硬編碼優(yōu)化為dp適配) int height = dp2px(350); // 高度:350dp float alpha = 0.7f; // 透明度:70% // 4. 通過工具類創(chuàng)建Dialog floatingDialog = FloatingDialogHelper.createFloatingDialog( this, R.layout.dialog_layout, // 你的Dialog自定義布局ID x, y, width, height, alpha, true, // 點擊返回鍵取消 false // 點擊外部不取消(可按需修改) ); // 5. (可選)操作Dialog內控件(通過ViewBinding) dialogBinding = DialogLayoutBinding.bind(floatingDialog.findViewById(android.R.id.content)); // 示例:設置Dialog標題(需在dialog_layout.xml中定義tv_dialog_title) // dialogBinding.tvDialogTitle.setText("優(yōu)化后的懸浮Dialog"); // 6. 顯示Dialog floatingDialog.show(); } /** * 更新點擊計數(示例業(yè)務邏輯) */ private void updateClickCount() { clickCount++; String tip = String.format("已點擊 %d 次", clickCount); Toast.makeText(this, tip, Toast.LENGTH_SHORT).show(); activityBinding.tvShow.setText(tip); // 更新Activity文本顯示 } /** * 跳轉到懸浮窗權限設置頁面 */ private void jumpToOverlaySetting() { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse("package:" + getPackageName())); // 直接定位到當前App權限頁 startActivityForResult(intent, 1001); // 1001為請求碼(可自定義) } /** * dp轉px(屏幕適配核心方法) * @param dpValue 需要轉換的dp值 * @return 轉換后的px值(適配不同屏幕密度) */ private int dp2px(float dpValue) { final float scale = getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); // +0.5f避免四舍五入誤差 } /** * 生命周期管理:避免內存泄漏(關鍵?。? */ @Override protected void onDestroy() { super.onDestroy(); // 銷毀Dialog并釋放引用 if (floatingDialog != null && floatingDialog.isShowing()) { floatingDialog.dismiss(); floatingDialog = null; } // 釋放ViewBinding引用 activityBinding = null; dialogBinding = null; } /** * 權限申請結果回調 */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // 處理懸浮窗權限申請結果 if (requestCode == 1001) { if (FloatingDialogHelper.hasOverlayPermission(this)) { showFloatingDialog(); // 授權成功,直接顯示Dialog } else { Toast.makeText(this, "未開啟懸浮窗權限,無法顯示Dialog", Toast.LENGTH_SHORT).show(); } } } }
四、關鍵優(yōu)化細節(jié)說明
1. 高版本兼容性解決方案
Android 8.0+ 必須使用?TYPE_APPLICATION_OVERLAY?替代過時的?TYPE_SYSTEM_OVERLAY,否則直接崩潰
6.0+ 需動態(tài)申請?SYSTEM_ALERT_WINDOW?權限,不能僅依賴 Manifest 聲明
權限申請后通過?onActivityResult?回調檢查,確保用戶授權后再顯示 Dialog
2. 屏幕適配優(yōu)化(解決硬編碼問題)
原代碼中?x=100、width=700?等硬編碼 px 值,在不同屏幕密度設備上顯示效果差異極大
優(yōu)化為?dp2px?方法,將 dp 值轉為 px,保證在手機、平板等設備上顯示比例一致
3. 內存泄漏防護(核心改進)
在?onDestroy?中銷毀 Dialog 并置空引用,避免 Dialog 持有 Activity 導致內存泄漏
使用 ViewBinding 替代 findViewById,減少控件引用泄漏風險
工具類中使用局部變量創(chuàng)建 Dialog,不持有 Context 強引用
4. 靈活性增強(滿足多場景需求)
支持自定義 Dialog 位置(x/y 偏移)、寬高、透明度
可配置點擊返回鍵 / 外部是否取消 Dialog
通過 DialogBinding 直接操作 Dialog 內控件,無需重復 findViewById
5. 代碼規(guī)范優(yōu)化
類名、變量名遵循 Java 命名規(guī)范(如?clickCount?替代?clickcounts)
分離關注點:Activity 負責業(yè)務邏輯,工具類負責 Dialog 創(chuàng)建,降低耦合
補充詳細注釋,包括方法功能、參數說明、關鍵邏輯解釋,便于維護
用 Lambda 表達式簡化點擊事件,減少匿名內部類代碼冗余
五、使用注意事項
1. 自定義 Dialog 布局(dialog_layout.xml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" android:orientation="vertical" android:padding="16dp"> <!-- 示例:Dialog標題 --> <TextView android:id="@+id/tv_dialog_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="懸浮Dialog示例" android:textSize="18sp" android:textStyle="bold" /> <!-- 可添加其他控件:按鈕、圖片、輸入框等 --> </LinearLayout>
2. 樣式自定義(可選)
如需優(yōu)化 Dialog 外觀,可創(chuàng)建自定義 Theme:
<!-- 自定義Dialog Theme(在styles.xml中添加) --> <style name="TransparentDialog" parent="Theme.AppCompat.Dialog"> <item name="android:windowBackground">@android:color/transparent</item> <!-- 透明背景 --> <item name="android:windowNoTitle">true</item> <!-- 隱藏標題欄 --> <item name="android:windowIsFloating">true</item> <!-- 懸浮樣式 --> </style>