android 自定义Dialog多种方式
代码如下
package com.nyw.mvvmmode.widget;import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;import com.nyw.mvvmmode.R;import java.lang.reflect.Field;/*** 自定义基类 Dialog(兼容 Android 16+)* 功能:* - 软键盘适配(自动上移+点击空白隐藏)* - 刘海屏适配(华为/小米/OPPO/vivo/Android 9.0+)* - 沉浸式全屏+半透明状态栏* - 位置控制(顶部/底部/居中)* - 全屏模式* - 动画支持* - 自适应宽高*/
public abstract class BaseDialog extends Dialog {protected Context mContext;private int mLayoutId;private boolean mCancelable = true;private boolean mCanceledOnTouchOutside = true;private int mGravity = Gravity.CENTER;private int mAnimationStyle = R.style.BaseDialogAnim;private boolean mFullScreen = false;private boolean mImmersive = false;private boolean mNotchScreen = false;private boolean mKeyboardAdapt = true; // 默认开启软键盘适配public BaseDialog(@NonNull Context context, @LayoutRes int layoutId) {super(context, R.style.BaseDialogStyle);this.mContext = context;this.mLayoutId = layoutId;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(mLayoutId);initView();initData();setWindowAttributes();// 初始化软键盘相关适配initKeyboardAdapt();}/*** 初始化View(子类实现)*/protected abstract void initView();/*** 初始化数据(子类实现)*/protected abstract void initData();/*** 设置Window核心属性*/protected void setWindowAttributes() {Window window = getWindow();if (window == null) return;// 背景透明(解决圆角阴影问题)window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));WindowManager.LayoutParams params = window.getAttributes();// 宽高设置if (mFullScreen) {params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;} else {params.width = getScreenWidth() - dp2px(48); // 左右留边params.height = WindowManager.LayoutParams.WRAP_CONTENT;// 限制最大高度(避免小屏手机内容溢出)int maxHeight = (int) (getScreenHeight() * 0.8);params.height = Math.min(params.height, maxHeight);}// 位置设置params.gravity = mGravity;// 软键盘适配:软键盘弹出时自动上移弹窗if (mKeyboardAdapt) {params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;}window.setAttributes(params);// 动画设置window.setWindowAnimations(mAnimationStyle);// 沉浸式状态栏if (mImmersive) setImmersiveStatusBar(window);// 刘海屏适配if (mNotchScreen) setNotchScreen(window);}/*** 软键盘适配初始化:点击空白处隐藏软键盘*/private void initKeyboardAdapt() {if (!mKeyboardAdapt) return;// 给弹窗根布局设置触摸监听View rootView = findViewById(android.R.id.content);if (rootView != null) {rootView.setOnTouchListener((v, event) -> {// 点击空白区域(非EditText)时隐藏软键盘if (event.getAction() == MotionEvent.ACTION_DOWN) {View focusView = getCurrentFocus();if (focusView instanceof EditText) {// 判断点击位置是否在EditText外if (!isTouchInView(event, focusView)) {hideSoftKeyboard(focusView);// 清除焦点(避免再次点击时软键盘重新弹出)focusView.clearFocus();}}}return false;});}}/*** 判断触摸点是否在View内部*/private boolean isTouchInView(MotionEvent event, View view) {if (view == null) return false;// 获取View的坐标范围int[] viewLocation = new int[2];view.getLocationOnScreen(viewLocation);int left = viewLocation[0];int top = viewLocation[1];int right = left + view.getWidth();int bottom = top + view.getHeight();// 判断触摸点是否在范围内float x = event.getRawX();float y = event.getRawY();return x >= left && x <= right && y >= top && y <= bottom;}/*** 隐藏软键盘*/protected void hideSoftKeyboard(View view) {if (view == null) return;InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) {imm.hideSoftInputFromWindow(view.getWindowToken(), 0);}}/*** 显示软键盘(子类可调用,例如主动唤起输入框)*/protected void showSoftKeyboard(EditText editText) {if (editText == null) return;editText.requestFocus(); // 先获取焦点InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) {imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);}}// ---------------------- 原有核心功能 ----------------------/*** 沉浸式状态栏(半透明)*/private void setImmersiveStatusBar(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 状态栏文字深色(如需浅色:View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);window.setStatusBarColor(Color.TRANSPARENT);} else {window.setStatusBarColor(Color.parseColor("#33000000")); // 半透明黑}} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);}}/*** 刘海屏适配(兼容各厂商)*/private void setNotchScreen(Window window) {// Android 9.0+ 官方适配if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {WindowManager.LayoutParams lp = window.getAttributes();lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;window.setAttributes(lp);}// 厂商适配(华为/小米/OPPO/vivo)try {setVendorNotch(window);} catch (Exception e) {e.printStackTrace();}}/*** 各厂商刘海屏适配(反射实现)*/private void setVendorNotch(Window window) throws Exception {WindowManager.LayoutParams lp = window.getAttributes();Class<?> lpClass = lp.getClass();Field cutoutField = lpClass.getDeclaredField("layoutInDisplayCutoutMode");cutoutField.setAccessible(true);cutoutField.setInt(lp, 1); // 允许内容延伸到刘海区域window.setAttributes(lp);}/*** 获取屏幕宽度*/protected int getScreenWidth() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().widthPixels: wm.getDefaultDisplay().getWidth();}/*** 获取屏幕高度*/protected int getScreenHeight() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().heightPixels: wm.getDefaultDisplay().getHeight();}/*** dp转px*/protected int dp2px(float dpValue) {return (int) (dpValue * mContext.getResources().getDisplayMetrics().density + 0.5f);}// ---------------------- 对外API ----------------------/** 设置弹窗位置(Gravity.TOP/BOTTOM/CENTER) */public void setGravity(int gravity) {this.mGravity = gravity;setWindowAttributes();}/** 设置弹窗动画 */public void setAnimationStyle(int style) {this.mAnimationStyle = style;setWindowAttributes();}/** 设置是否全屏 */public void setFullScreen(boolean fullScreen) {this.mFullScreen = fullScreen;setWindowAttributes();}/** 设置是否沉浸式(状态栏半透明) */public void setImmersive(boolean immersive) {this.mImmersive = immersive;setWindowAttributes();}/** 设置是否适配刘海屏 */public void setNotchScreen(boolean notchScreen) {this.mNotchScreen = notchScreen;setWindowAttributes();}/** 设置是否开启软键盘适配(默认开启) */public void setKeyboardAdapt(boolean keyboardAdapt) {this.mKeyboardAdapt = keyboardAdapt;setWindowAttributes();}@Overridepublic void setCancelable(boolean flag) {super.setCancelable(flag);this.mCancelable = flag;}@Overridepublic void setCanceledOnTouchOutside(boolean cancel) {super.setCanceledOnTouchOutside(cancel);this.mCanceledOnTouchOutside = cancel;}@Overridepublic <T extends View> T findViewById(int id) {return super.findViewById(id);}
}
1️⃣ 在 res/values/styles.xml
中添加动画样式
<!-- BaseDialog 默认动画(淡入淡出) -->
<style name="BaseDialogAnim"><item name="android:windowEnterAnimation">@anim/dialog_fade_in</item><item name="android:windowExitAnimation">@anim/dialog_fade_out</item>
</style><!-- 底部弹窗动画(从下往上滑入) -->
<style name="DialogBottomAnim"><item name="android:windowEnterAnimation">@anim/slide_in_bottom</item><item name="android:windowExitAnimation">@anim/slide_out_bottom</item>
</style><!-- 顶部弹窗动画(从上往下滑入) -->
<style name="DialogTopAnim"><item name="android:windowEnterAnimation">@anim/slide_in_top</item><item name="android:windowExitAnimation">@anim/slide_out_top</item>
</style>
2️⃣ 在 res/anim/
目录下创建动画文件
dialog_fade_in.xml(淡入)
xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromAlpha="0.0"android:toAlpha="1.0" />
dialog_fade_out.xml(淡出)
xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromAlpha="1.0"android:toAlpha="0.0" />
slide_in_bottom.xml(底部滑入)
xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromYDelta="100%p"android:toYDelta="0" />
slide_out_bottom.xml(底部滑出)
xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromYDelta="0"android:toYDelta="100%p" />
slide_in_top.xml(顶部滑入)
xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromYDelta="-100%p"android:toYDelta="0" />
slide_out_top.xml(顶部滑出)
xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"android:duration="300"android:fromYDelta="0"android:toYDelta="-100%p" />
3️⃣ 用法
// 默认动画(淡入淡出)
BaseDialog dialog = new MyDialog(this);
dialog.show();// 底部弹窗动画
dialog.setAnimationStyle(R.style.DialogBottomAnim);
dialog.setGravity(Gravity.BOTTOM);
dialog.show();// 顶部弹窗动画
dialog.setAnimationStyle(R.style.DialogTopAnim);
dialog.setGravity(Gravity.TOP);
dialog.show();
4️⃣ 注意事项
- 动画时长我设置的是 300ms,你可以根据需求改成 200ms 或 400ms。
- 如果你的弹窗是全屏的,建议用
DialogBottomAnim
或DialogTopAnim
,效果更像原生的BottomSheetDialog
。 - 如果是居中的对话框,用
BaseDialogAnim
淡入淡出效果更自然。
弹窗圆角背景(示例)
为了让弹窗有圆角,我们需要自己定义一个 shape 背景:
res/drawable/dialog_bg.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><!-- 白色背景 --><solid android:color="@android:color/white" /><!-- 圆角 --><corners android:radius="12dp" /><!-- 边框(可选) --><strokeandroid:width="1dp"android:color="#EEEEEE" />
</shape>
在布局中引用圆角背景
你的弹窗布局根节点加:
xml
android:background="@drawable/dialog_bg"
示例:
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@drawable/dialog_bg"android:orientation="vertical"android:padding="16dp"><!-- 内容 -->
</LinearLayout>
为什么需要 BaseDialogStyle
?
- 去掉标题栏:否则系统会默认加一个标题栏,很难看。
- 背景透明:这样我们自定义的圆角背景才能生效。
- 背景变暗:让弹窗出现时背景半透明,突出弹窗内容。
- 默认动画:关联
BaseDialogAnim
,弹窗出现 / 消失时有动画。
使用示例
public class TestDialog extends BaseDialog {public TestDialog(Context context) {super(context, R.layout.dialog_test);}@Overrideprotected void initView() {findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());}@Overrideprotected void initData() {}
}// 调用
TestDialog dialog = new TestDialog(this);
dialog.setGravity(Gravity.BOTTOM);
dialog.setAnimationStyle(R.style.DialogBottomAnim);
dialog.setImmersive(true);
dialog.setNotchScreen(true);
dialog.show();
✅ 这样整合后,你直接复制这些文件就能用,功能包括:
- 位置控制(居中 / 顶部 / 底部)
- 全屏模式
- 沉浸式状态栏
- 刘海屏适配
- 软键盘适配
- 动画效果(淡入淡出 / 上下滑动)
另外增加一个安全模式。在 BaseDialog 中加一个 “安全模式”,开启后点击弹窗外部不会关闭弹窗,防止用户误触关闭(比如隐私协议、强制更新等必须操作的场景),同时保留之前所有功能(位置控制、全屏、沉浸式、刘海屏、软键盘适配等)。
代码如下
package com.nyw.mvvmmode.widget;import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;import java.lang.reflect.Field;/*** 自定义基类 Dialog(兼容 Android 16+)* 功能:* - 安全模式(点击外部不关闭弹窗)* - 位置控制(顶部 / 底部 / 居中)* - 全屏模式* - 半透明状态栏 + 沉浸式* - 刘海屏适配(华为/小米/OPPO/vivo/Android 9.0+)* - 软键盘适配(自动上移 + 点击空白隐藏)* - 动画支持(淡入淡出 / 上下滑动)*/
public abstract class BaseDialog extends Dialog {protected Context mContext;private int mLayoutId;private boolean mCancelable = true; // 按返回键是否关闭private boolean mCanceledOnTouchOutside = true; // 点击外部是否关闭private boolean mSafeMode = false; // 安全模式(点击外部和按返回键都不关闭)private int mGravity = Gravity.CENTER;private int mAnimationStyle = R.style.BaseDialogAnim;private boolean mFullScreen = false;private boolean mImmersive = false;private boolean mNotchScreen = false;private boolean mKeyboardAdapt = true;public BaseDialog(@NonNull Context context, @LayoutRes int layoutId) {super(context, R.style.BaseDialogStyle);this.mContext = context;this.mLayoutId = layoutId;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(mLayoutId);initView();initData();setWindowAttributes();initKeyboardAdapt();}protected abstract void initView();protected abstract void initData();/*** 设置弹窗宽高、位置、动画、沉浸式、刘海屏适配*/protected void setWindowAttributes() {Window window = getWindow();if (window == null) return;window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));WindowManager.LayoutParams params = window.getAttributes();if (mFullScreen) {params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;} else {params.width = getScreenWidth() - dp2px(48);params.height = WindowManager.LayoutParams.WRAP_CONTENT;int maxHeight = (int) (getScreenHeight() * 0.8);if (params.height > maxHeight) {params.height = maxHeight;}}params.gravity = mGravity;if (mKeyboardAdapt) {params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;}window.setAttributes(params);window.setWindowAnimations(mAnimationStyle);if (mImmersive) setImmersiveStatusBar(window);if (mNotchScreen) setNotchScreen(window);}/*** 点击空白隐藏软键盘*/private void initKeyboardAdapt() {if (!mKeyboardAdapt) return;View rootView = findViewById(android.R.id.content);if (rootView != null) {rootView.setOnTouchListener((v, event) -> {if (event.getAction() == MotionEvent.ACTION_DOWN) {View focusView = getCurrentFocus();if (focusView instanceof EditText) {if (!isTouchInView(event, focusView)) {hideSoftKeyboard(focusView);focusView.clearFocus();}}}return false;});}}private boolean isTouchInView(MotionEvent event, View view) {if (view == null) return false;int[] loc = new int[2];view.getLocationOnScreen(loc);int left = loc[0], top = loc[1], right = left + view.getWidth(), bottom = top + view.getHeight();float x = event.getRawX(), y = event.getRawY();return x >= left && x <= right && y >= top && y <= bottom;}protected void hideSoftKeyboard(View view) {if (view == null) return;InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.hideSoftInputFromWindow(view.getWindowToken(), 0);}protected void showSoftKeyboard(EditText editText) {if (editText == null) return;editText.requestFocus();InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);}/*** 沉浸式状态栏*/private void setImmersiveStatusBar(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);window.setStatusBarColor(Color.TRANSPARENT);} else {window.setStatusBarColor(Color.parseColor("#33000000"));}} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);}}/*** 刘海屏适配*/private void setNotchScreen(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {WindowManager.LayoutParams lp = window.getAttributes();lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;window.setAttributes(lp);}try {setVendorNotch(window);} catch (Exception e) {e.printStackTrace();}}private void setVendorNotch(Window window) throws Exception {WindowManager.LayoutParams lp = window.getAttributes();Class<?> lpClass = lp.getClass();Field cutoutField = lpClass.getDeclaredField("layoutInDisplayCutoutMode");cutoutField.setAccessible(true);cutoutField.setInt(lp, 1);window.setAttributes(lp);}protected int getScreenWidth() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().widthPixels: wm.getDefaultDisplay().getWidth();}protected int getScreenHeight() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().heightPixels: wm.getDefaultDisplay().getHeight();}protected int dp2px(float dpValue) {return (int) (dpValue * mContext.getResources().getDisplayMetrics().density + 0.5f);}/*** 设置安全模式* @param safeMode true=开启(点击外部和按返回键都不关闭)*/public void setSafeMode(boolean safeMode) {this.mSafeMode = safeMode;setCancelable(!safeMode); // 安全模式下禁止按返回键关闭setCanceledOnTouchOutside(!safeMode); // 安全模式下禁止点击外部关闭}public void setGravity(int gravity) {this.mGravity = gravity;setWindowAttributes();}public void setAnimationStyle(int style) {this.mAnimationStyle = style;setWindowAttributes();}public void setFullScreen(boolean fullScreen) {this.mFullScreen = fullScreen;setWindowAttributes();}public void setImmersive(boolean immersive) {this.mImmersive = immersive;setWindowAttributes();}public void setNotchScreen(boolean notchScreen) {this.mNotchScreen = notchScreen;setWindowAttributes();}public void setKeyboardAdapt(boolean keyboardAdapt) {this.mKeyboardAdapt = keyboardAdapt;setWindowAttributes();}@Overridepublic void setCancelable(boolean flag) {super.setCancelable(flag);this.mCancelable = flag;}@Overridepublic void setCanceledOnTouchOutside(boolean cancel) {super.setCanceledOnTouchOutside(cancel);this.mCanceledOnTouchOutside = cancel;}@Overridepublic <T extends View> T findViewById(int id) {return super.findViewById(id);}
}
安全模式使用方法
// 创建弹窗
AgreementDialog dialog = new AgreementDialog(this);// 开启安全模式(点击外部和按返回键都不能关闭)
dialog.setSafeMode(true);// 其他设置
dialog.setGravity(Gravity.CENTER);
dialog.setAnimationStyle(R.style.BaseDialogAnim);// 显示
dialog.show();
3️⃣ 安全模式原理
- 点击外部不关闭:通过
setCanceledOnTouchOutside(false)
实现 - 按返回键不关闭:通过
setCancelable(false)
实现 - 我在
setSafeMode(boolean)
方法中统一封装了这两个设置,调用一次即可开启 / 关闭安全模式 4️⃣ 应用场景
- 隐私政策 & 用户协议弹窗(必须让用户选择 “同意” 才能进入应用)
- 强制更新弹窗(不更新不能使用应用)
- 重要提示 / 警告(必须用户确认)、
✅ 这样你的 BaseDialog 现在就有了:
- 安全模式(防止误触关闭)
- 位置控制(居中 / 顶部 / 底部)
- 全屏模式
- 沉浸式状态栏
- 刘海屏适配
- 软键盘适配
- 动画效果
加一个倒计时自动关闭功能,比如 “5 秒后自动关闭”,用于广告弹窗或提示弹窗,这样体验会更好。
代码如下
package com.nyw.mvvmmode.widget;import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;import com.nyw.mvvmmode.R;import java.lang.reflect.Field;/*** 自定义基类 Dialog(兼容 Android 16+)* 功能:* - 安全模式(点击外部不关闭弹窗)* - 倒计时自动关闭* - 位置控制(顶部 / 底部 / 居中)* - 全屏模式* - 半透明状态栏 + 沉浸式* - 刘海屏适配(华为/小米/OPPO/vivo/Android 9.0+)* - 软键盘适配(自动上移 + 点击空白隐藏)* - 动画支持(淡入淡出 / 上下滑动)*/
public abstract class BaseDialog extends Dialog {protected Context mContext;private int mLayoutId;private boolean mCancelable = true;private boolean mCanceledOnTouchOutside = true;private boolean mSafeMode = false;private int mGravity = Gravity.CENTER;private int mAnimationStyle = R.style.BaseDialogAnim;private boolean mFullScreen = false;private boolean mImmersive = false;private boolean mNotchScreen = false;private boolean mKeyboardAdapt = true;private CountDownTimer mCountDownTimer;private TextView mCountDownView; // 显示倒计时的控件private int mAutoDismissSeconds = 0; // 自动关闭倒计时(秒)public BaseDialog(@NonNull Context context, @LayoutRes int layoutId) {super(context, R.style.BaseDialogStyle);this.mContext = context;this.mLayoutId = layoutId;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(mLayoutId);initView();initData();setWindowAttributes();initKeyboardAdapt();startCountDownTimer();}protected abstract void initView();protected abstract void initData();/*** 设置弹窗宽高、位置、动画、沉浸式、刘海屏适配*/protected void setWindowAttributes() {Window window = getWindow();if (window == null) return;window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));WindowManager.LayoutParams params = window.getAttributes();if (mFullScreen) {params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;} else {params.width = getScreenWidth() - dp2px(48);params.height = WindowManager.LayoutParams.WRAP_CONTENT;int maxHeight = (int) (getScreenHeight() * 0.8);if (params.height > maxHeight) {params.height = maxHeight;}}params.gravity = mGravity;if (mKeyboardAdapt) {params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;}window.setAttributes(params);window.setWindowAnimations(mAnimationStyle);if (mImmersive) setImmersiveStatusBar(window);if (mNotchScreen) setNotchScreen(window);}/*** 点击空白隐藏软键盘*/private void initKeyboardAdapt() {if (!mKeyboardAdapt) return;View rootView = findViewById(android.R.id.content);if (rootView != null) {rootView.setOnTouchListener((v, event) -> {if (event.getAction() == MotionEvent.ACTION_DOWN) {View focusView = getCurrentFocus();if (focusView instanceof EditText) {if (!isTouchInView(event, focusView)) {hideSoftKeyboard(focusView);focusView.clearFocus();}}}return false;});}}private boolean isTouchInView(MotionEvent event, View view) {if (view == null) return false;int[] loc = new int[2];view.getLocationOnScreen(loc);int left = loc[0], top = loc[1], right = left + view.getWidth(), bottom = top + view.getHeight();float x = event.getRawX(), y = event.getRawY();return x >= left && x <= right && y >= top && y <= bottom;}protected void hideSoftKeyboard(View view) {if (view == null) return;InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.hideSoftInputFromWindow(view.getWindowToken(), 0);}protected void showSoftKeyboard(EditText editText) {if (editText == null) return;editText.requestFocus();InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);}/*** 沉浸式状态栏*/private void setImmersiveStatusBar(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);window.setStatusBarColor(Color.TRANSPARENT);} else {window.setStatusBarColor(Color.parseColor("#33000000"));}} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);}}/*** 刘海屏适配*/private void setNotchScreen(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {WindowManager.LayoutParams lp = window.getAttributes();lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;window.setAttributes(lp);}try {setVendorNotch(window);} catch (Exception e) {e.printStackTrace();}}private void setVendorNotch(Window window) throws Exception {WindowManager.LayoutParams lp = window.getAttributes();Class<?> lpClass = lp.getClass();Field cutoutField = lpClass.getDeclaredField("layoutInDisplayCutoutMode");cutoutField.setAccessible(true);cutoutField.setInt(lp, 1);window.setAttributes(lp);}protected int getScreenWidth() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().widthPixels: wm.getDefaultDisplay().getWidth();}protected int getScreenHeight() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().heightPixels: wm.getDefaultDisplay().getHeight();}protected int dp2px(float dpValue) {return (int) (dpValue * mContext.getResources().getDisplayMetrics().density + 0.5f);}/*** 设置安全模式* @param safeMode true=开启(点击外部和按返回键都不关闭)*/public void setSafeMode(boolean safeMode) {this.mSafeMode = safeMode;setCancelable(!safeMode);setCanceledOnTouchOutside(!safeMode);}/*** 设置倒计时自动关闭* @param seconds 倒计时秒数* @param countDownView 显示倒计时的TextView(可以为null)*/public void setAutoDismiss(int seconds, TextView countDownView) {this.mAutoDismissSeconds = seconds;this.mCountDownView = countDownView;}/*** 启动倒计时*/private void startCountDownTimer() {if (mAutoDismissSeconds > 0) {cancelCountDownTimer();mCountDownTimer = new CountDownTimer(mAutoDismissSeconds * 1000L, 1000L) {@Overridepublic void onTick(long millisUntilFinished) {int seconds = (int) (millisUntilFinished / 1000);if (mCountDownView != null) {mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));}}@Overridepublic void onFinish() {dismiss();}}.start();}}/*** 取消倒计时*/private void cancelCountDownTimer() {if (mCountDownTimer != null) {mCountDownTimer.cancel();mCountDownTimer = null;}}@Overridepublic void dismiss() {cancelCountDownTimer();super.dismiss();}public void setGravity(int gravity) {this.mGravity = gravity;setWindowAttributes();}public void setAnimationStyle(int style) {this.mAnimationStyle = style;setWindowAttributes();}public void setFullScreen(boolean fullScreen) {this.mFullScreen = fullScreen;setWindowAttributes();}public void setImmersive(boolean immersive) {this.mImmersive = immersive;setWindowAttributes();}public void setNotchScreen(boolean notchScreen) {this.mNotchScreen = notchScreen;setWindowAttributes();}public void setKeyboardAdapt(boolean keyboardAdapt) {this.mKeyboardAdapt = keyboardAdapt;setWindowAttributes();}@Overridepublic void setCancelable(boolean flag) {super.setCancelable(flag);this.mCancelable = flag;}@Overridepublic void setCanceledOnTouchOutside(boolean cancel) {super.setCanceledOnTouchOutside(cancel);this.mCanceledOnTouchOutside = cancel;}@Overridepublic <T extends View> T findViewById(int id) {return super.findViewById(id);}
}
使用示例(倒计时自动关闭弹窗)
public class AutoCloseDialog extends BaseDialog {public AutoCloseDialog(Context context) {super(context, R.layout.dialog_auto_close);}@Overrideprotected void initView() {TextView tvCountDown = findViewById(R.id.tv_count_down);// 设置5秒后自动关闭,并更新倒计时文本setAutoDismiss(5, tvCountDown);findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());}@Overrideprotected void initData() {}
}
布局示例(res/layout/dialog_auto_close.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="wrap_content"android:background="@drawable/dialog_bg"android:orientation="vertical"android:padding="16dp"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="这是一个自动关闭的弹窗"android:textColor="@color/black"android:textSize="18sp" /><TextViewandroid:id="@+id/tv_count_down"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="12dp"android:textColor="@color/gray"android:textSize="14sp" /><Buttonandroid:id="@+id/btn_close"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="12dp"android:text="立即关闭" /></LinearLayout>
倒计时功能说明
- setAutoDismiss(int seconds, TextView countDownView)
seconds
:倒计时秒数countDownView
:显示倒计时的控件(可为 null)
- 倒计时结束会自动调用
dismiss()
关闭弹窗 - 如果用户手动关闭弹窗,会自动取消倒计时
- 可在布局中添加一个
TextView
实时显示剩余时间
✅ 这样你的 BaseDialog 现在功能就非常全面了:
- 安全模式(防止误触关闭)
- 倒计时自动关闭(广告 / 提示弹窗)
- 位置控制(居中 / 顶部 / 底部)
- 全屏模式
- 沉浸式状态栏
- 刘海屏适配
- 软键盘适配
- 动画效果
BaseDialog 中加一个弹窗显示次数限制,并且支持同一个时间范围内、同一个弹窗的限制。
一、功能说明
我会帮你实现:
- 同一个弹窗类型:用唯一 ID(比如
dialog_privacy
、dialog_update
)来区分不同弹窗。 - 时间范围限制:比如一天、三天、一周内最多显示几次。
- 次数限制:在指定时间范围内,达到次数后不再显示。
- 数据持久化:用
SharedPreferences
保存弹窗显示记录,App 重启后依然有效。
二、修改后的 BaseDialog(增加显示次数限制)
package com.nyw.mvvmmode.widget;import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;import java.lang.reflect.Field;/*** 自定义基类 Dialog(兼容 Android 16+)* 功能:* - 安全模式(点击外部和按返回键都不关闭)* - 倒计时自动关闭(可更新倒计时文本)* - 弹窗显示次数限制(同一时间范围 & 同一弹窗)* - 位置控制(顶部 / 底部 / 居中)* - 全屏模式* - 半透明状态栏 + 沉浸式* - 刘海屏适配(Android 9.0+ 官方 & 华为/小米/OPPO/vivo)* - 软键盘适配(自动上移 + 点击空白隐藏)* - 动画支持(淡入淡出 / 上下滑动)*/
public abstract class BaseDialog extends Dialog {protected Context mContext; // 上下文private int mLayoutId; // 弹窗布局ID// 弹窗基本配置private boolean mCancelable = true; // 按返回键是否关闭private boolean mCanceledOnTouchOutside = true; // 点击外部是否关闭private boolean mSafeMode = false; // 安全模式(点击外部和按返回键都不关闭)private int mGravity = Gravity.CENTER; // 弹窗位置private int mAnimationStyle = R.style.BaseDialogAnim; // 弹窗动画private boolean mFullScreen = false; // 是否全屏private boolean mImmersive = false; // 是否沉浸式状态栏private boolean mNotchScreen = false; // 是否适配刘海屏private boolean mKeyboardAdapt = true; // 是否软键盘适配// 倒计时自动关闭private CountDownTimer mCountDownTimer;private TextView mCountDownView; // 显示倒计时的控件private int mAutoDismissSeconds = 0; // 自动关闭倒计时(秒)// 弹窗显示次数限制private String mDialogId; // 当前弹窗唯一IDprivate int mMaxShowCount = -1; // 时间范围内最大显示次数,-1表示无限制private long mTimeRangeMillis = 24 * 60 * 60 * 1000L; // 默认时间范围:24小时public BaseDialog(@NonNull Context context, @LayoutRes int layoutId) {super(context, R.style.BaseDialogStyle); // 使用自定义样式this.mContext = context;this.mLayoutId = layoutId;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(mLayoutId); // 设置布局initView(); // 初始化控件initData(); // 初始化数据setWindowAttributes(); // 设置窗口属性initKeyboardAdapt(); // 初始化软键盘适配startCountDownTimer(); // 启动倒计时}/*** 初始化View(子类实现)*/protected abstract void initView();/*** 初始化数据(子类实现)*/protected abstract void initData();/*** 设置Window属性(宽高、位置、动画、沉浸式、刘海屏等)*/protected void setWindowAttributes() {Window window = getWindow();if (window == null) return;// 背景透明(让圆角生效)window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));WindowManager.LayoutParams params = window.getAttributes();// 设置宽高if (mFullScreen) {params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;} else {params.width = getScreenWidth() - dp2px(48); // 左右留边params.height = WindowManager.LayoutParams.WRAP_CONTENT;// 限制最大高度int maxHeight = (int) (getScreenHeight() * 0.8);if (params.height > maxHeight) {params.height = maxHeight;}}// 设置位置params.gravity = mGravity;// 软键盘适配if (mKeyboardAdapt) {params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;}window.setAttributes(params);// 设置动画window.setWindowAnimations(mAnimationStyle);// 沉浸式状态栏if (mImmersive) setImmersiveStatusBar(window);// 刘海屏适配if (mNotchScreen) setNotchScreen(window);}/*** 初始化软键盘适配(点击空白隐藏软键盘)*/private void initKeyboardAdapt() {if (!mKeyboardAdapt) return;View rootView = findViewById(android.R.id.content);if (rootView != null) {rootView.setOnTouchListener((v, event) -> {if (event.getAction() == MotionEvent.ACTION_DOWN) {View focusView = getCurrentFocus();if (focusView instanceof EditText) {if (!isTouchInView(event, focusView)) {hideSoftKeyboard(focusView);focusView.clearFocus();}}}return false;});}}/*** 判断触摸点是否在View内*/private boolean isTouchInView(MotionEvent event, View view) {if (view == null) return false;int[] loc = new int[2];view.getLocationOnScreen(loc);int left = loc[0], top = loc[1], right = left + view.getWidth(), bottom = top + view.getHeight();float x = event.getRawX(), y = event.getRawY();return x >= left && x <= right && y >= top && y <= bottom;}/*** 隐藏软键盘*/protected void hideSoftKeyboard(View view) {if (view == null) return;InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.hideSoftInputFromWindow(view.getWindowToken(), 0);}/*** 显示软键盘*/protected void showSoftKeyboard(EditText editText) {if (editText == null) return;editText.requestFocus();InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);}/*** 设置沉浸式状态栏(半透明)*/private void setImmersiveStatusBar(Window window) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);window.setStatusBarColor(Color.TRANSPARENT);} else {window.setStatusBarColor(Color.parseColor("#33000000")); // 半透明黑}} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);}}/*** 刘海屏适配*/private void setNotchScreen(Window window) {// Android 9.0+ 官方适配if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {WindowManager.LayoutParams lp = window.getAttributes();lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;window.setAttributes(lp);}// 厂商适配(华为/小米/OPPO/vivo)try {setVendorNotch(window);} catch (Exception e) {e.printStackTrace();}}/*** 厂商刘海屏适配(反射)*/private void setVendorNotch(Window window) throws Exception {WindowManager.LayoutParams lp = window.getAttributes();Class<?> lpClass = lp.getClass();Field cutoutField = lpClass.getDeclaredField("layoutInDisplayCutoutMode");cutoutField.setAccessible(true);cutoutField.setInt(lp, 1); // 允许内容延伸到刘海区域window.setAttributes(lp);}/*** 获取屏幕宽度*/protected int getScreenWidth() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().widthPixels: wm.getDefaultDisplay().getWidth();}/*** 获取屏幕高度*/protected int getScreenHeight() {WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);if (wm == null) return 0;return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1? mContext.getResources().getDisplayMetrics().heightPixels: wm.getDefaultDisplay().getHeight();}/*** dp转px*/protected int dp2px(float dpValue) {return (int) (dpValue * mContext.getResources().getDisplayMetrics().density + 0.5f);}/*** 设置安全模式* @param safeMode true=开启(点击外部和按返回键都不关闭)*/public void setSafeMode(boolean safeMode) {this.mSafeMode = safeMode;setCancelable(!safeMode);setCanceledOnTouchOutside(!safeMode);}/*** 设置倒计时自动关闭* @param seconds 倒计时秒数* @param countDownView 显示倒计时的TextView(可以为null)*/public void setAutoDismiss(int seconds, TextView countDownView) {this.mAutoDismissSeconds = seconds;this.mCountDownView = countDownView;}/*** 启动倒计时*/private void startCountDownTimer() {if (mAutoDismissSeconds > 0) {cancelCountDownTimer();mCountDownTimer = new CountDownTimer(mAutoDismissSeconds * 1000L, 1000L) {@Overridepublic void onTick(long millisUntilFinished) {int seconds = (int) (millisUntilFinished / 1000);if (mCountDownView != null) {mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));}}@Overridepublic void onFinish() {dismiss();}}.start();}}/*** 取消倒计时*/private void cancelCountDownTimer() {if (mCountDownTimer != null) {mCountDownTimer.cancel();mCountDownTimer = null;}}/*** 设置弹窗显示次数限制* @param dialogId 弹窗唯一标识* @param maxShowCount 时间范围内最大显示次数,-1表示无限制* @param timeRangeMillis 时间范围(毫秒),例如一天=24*60*60*1000*/public void setShowCountLimit(String dialogId, int maxShowCount, long timeRangeMillis) {this.mDialogId = dialogId;this.mMaxShowCount = maxShowCount;this.mTimeRangeMillis = timeRangeMillis;}/*** 检查是否还能显示*/public boolean canShow() {if (mMaxShowCount < 0 || TextUtils.isEmpty(mDialogId)) {return true; // 没有设置限制}SharedPreferences sp = mContext.getSharedPreferences("dialog_show_count", Context.MODE_PRIVATE);long firstShowTime = sp.getLong(mDialogId + "_first_time", 0);int showCount = sp.getInt(mDialogId + "_count", 0);long currentTime = System.currentTimeMillis();// 如果超过时间范围,重置计数if (firstShowTime == 0 || currentTime - firstShowTime > mTimeRangeMillis) {sp.edit().putLong(mDialogId + "_first_time", currentTime).putInt(mDialogId + "_count", 1).apply();return true;}// 如果没超过次数限制if (showCount < mMaxShowCount) {sp.edit().putInt(mDialogId + "_count", showCount + 1).apply();return true;}return false; // 超过限制,不能显示}@Overridepublic void show() {if (canShow()) {super.show();}}@Overridepublic void dismiss() {cancelCountDownTimer(); // 关闭时取消倒计时super.dismiss();}// ========== 对外设置方法 ==========public void setGravity(int gravity) {this.mGravity = gravity;setWindowAttributes();}public void setAnimationStyle(int style) {this.mAnimationStyle = style;setWindowAttributes();}public void setFullScreen(boolean fullScreen) {this.mFullScreen = fullScreen;setWindowAttributes();}public void setImmersive(boolean immersive) {this.mImmersive = immersive;setWindowAttributes();}public void setNotchScreen(boolean notchScreen) {this.mNotchScreen = notchScreen;setWindowAttributes();}public void setKeyboardAdapt(boolean keyboardAdapt) {this.mKeyboardAdapt = keyboardAdapt;setWindowAttributes();}@Overridepublic void setCancelable(boolean flag) {super.setCancelable(flag);this.mCancelable = flag;}@Overridepublic void setCanceledOnTouchOutside(boolean cancel) {super.setCanceledOnTouchOutside(cancel);this.mCanceledOnTouchOutside = cancel;}@Overridepublic <T extends View> T findViewById(int id) {return super.findViewById(id);}
}
三、使用示例
java
运行
// 创建弹窗
AgreementDialog dialog = new AgreementDialog(this);// 设置弹窗ID和显示次数限制:
// ID = "dialog_privacy"
// 1天内最多显示1次
dialog.setShowCountLimit("dialog_privacy", 1, 24 * 60 * 60 * 1000);// 显示(内部会检查是否达到限制)
dialog.show();
四、功能说明
- 同一个弹窗:用
dialogId
区分,比如"dialog_privacy"
、"dialog_update"
。 - 时间范围:
timeRangeMillis
控制,比如 1 天 = 86400000ms,3 天 = 259200000ms。 - 次数限制:
maxShowCount
控制,比如 1 表示一天内最多显示一次。 - 自动重置:超过时间范围后,计数会自动重置。
- 数据保存:用
SharedPreferences
存储,App 重启后依然有效。
五、应用场景
- 隐私政策弹窗:只在用户第一次打开 App 时显示。
- 广告弹窗:限制一天最多弹 2 次。
- 更新提示:限制一天最多提示一次。
✅ 这样你的 BaseDialog 现在就支持:
- 安全模式
- 倒计时自动关闭
- 弹窗显示次数限制(同一时间范围 & 同一弹窗)
- 位置控制
- 全屏模式
- 沉浸式状态栏
- 刘海屏适配
- 软键盘适配
- 动画效果
另外加几个功能
package com.nyw.mvvmmode.widget;import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;import com.nyw.mvvmmode.R;/*** 自定义基类 Dialog* 功能:* - 拖拽移动位置* - 靠近屏幕边缘自动磁吸* - 保存位置,下次打开自动恢复* - 安全模式(点击外部和按返回键都不关闭)* - 倒计时自动关闭* - 弹窗显示次数限制(同一时间范围 & 同一弹窗)* - 位置控制(顶部 / 底部 / 居中)* - 全屏模式* - 半透明状态栏 + 沉浸式* - 刘海屏适配* - 软键盘适配(点击空白隐藏软键盘)* - 动画支持*/
public abstract class BaseDialog extends Dialog {protected Context mContext; // 上下文private int mLayoutId; // 弹窗布局ID// 弹窗基本配置private boolean mCancelable = true; // 按返回键是否关闭private boolean mCanceledOnTouchOutside = true; // 点击外部是否关闭private boolean mSafeMode = false; // 安全模式(点击外部和按返回键都不关闭)private int mGravity = Gravity.CENTER; // 弹窗位置private int mAnimationStyle = R.style.BaseDialogAnim; // 弹窗动画private boolean mFullScreen = false; // 是否全屏private boolean mImmersive = false; // 是否沉浸式状态栏private boolean mNotchScreen = false; // 是否适配刘海屏private boolean mKeyboardAdapt = true; // 是否软键盘适配// 倒计时自动关闭private CountDownTimer mCountDownTimer;private TextView mCountDownView; // 显示倒计时的控件private int mAutoDismissSeconds = 0; // 自动关闭倒计时(秒)// 弹窗显示次数限制private String mDialogId; // 当前弹窗唯一IDprivate int mMaxShowCount = -1; // 时间范围内最大显示次数,-1表示无限制private long mTimeRangeMillis = 24 * 60 * 60 * 1000L; // 默认时间范围:24小时// 拖拽相关变量private float mTouchStartX;private float mTouchStartY;private int mWindowStartX;private int mWindowStartY;private boolean mDraggable = false; // 是否可拖拽private View mDragView; // 拖拽区域视图// 磁吸相关变量private boolean mMagneticEffect = false; // 是否启用磁吸效果private int mMagneticRange = 100; // 磁吸生效距离(像素)private int mScreenWidth; // 屏幕宽度private int mScreenHeight; // 屏幕高度// 位置保存相关变量private boolean mSavePosition = false; // 是否保存位置private String mPositionTag; // 弹窗唯一标识,用于保存位置public BaseDialog(@NonNull Context context, @LayoutRes int layoutId) {super(context, R.style.BaseDialogStyle); // 使用自定义样式this.mContext = context;this.mLayoutId = layoutId;// 获取屏幕宽高mScreenWidth = getScreenWidth();mScreenHeight = getScreenHeight();}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(mLayoutId); // 设置布局initView(); // 初始化控件initData(); // 初始化数据setWindowAttributes(); // 设置窗口属性initKeyboardAdapt(); // 初始化软键盘适配startCountDownTimer(); // 启动倒计时setupDragListener(); // 设置拖拽监听restoreSavedPosition(); // 恢复保存的位置}/*** 初始化View(子类实现)*/protected abstract void initView();/*** 初始化数据(子类实现)*/protected abstract void initData();/*** 设置Window属性(宽高、位置、动画、沉浸式、刘海屏等)*/protected void setWindowAttributes() {Window window = getWindow();if (window == null) return;// 背景透明(让圆角生效)window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));WindowManager.LayoutParams params = window.getAttributes();// 设置宽高if (mFullScreen) {params.width = WindowManager.LayoutParams.MATCH_PARENT;params.height = WindowManager.LayoutParams.MATCH_PARENT;} else {params.width = getScreenWidth() - dp2px(48); // 左右留边params.height = WindowManager.LayoutParams.WRAP_CONTENT;// 限制最大高度int maxHeight = (int) (getScreenHeight() * 0.8);if (params.height > maxHeight) {params.height = maxHeight;}}// 设置位置params.gravity = mGravity;// 软键盘适配if (mKeyboardAdapt) {params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;}window.setAttributes(params);// 设置动画window.setWindowAnimations(mAnimationStyle);// 沉浸式状态栏if (mImmersive) setImmersiveStatusBar(window);// 刘海屏适配if (mNotchScreen) setNotchScreen(window);}/*** 初始化软键盘适配(点击空白隐藏软键盘)*/private void initKeyboardAdapt() {if (!mKeyboardAdapt) return;View rootView = findViewById(android.R.id.content);if (rootView != null) {rootView.setOnTouchListener((v, event) -> {if (event.getAction() == MotionEvent.ACTION_DOWN) {View focusView = getCurrentFocus();if (focusView instanceof EditText) {if (!isTouchInView(event, focusView)) {hideSoftKeyboard(focusView);focusView.clearFocus();}}}return false;});}}/*** 判断触摸点是否在View内*/private boolean isTouchInView(MotionEvent event, View view) {if (view == null) return false;int[] loc = new int[2];view.getLocationOnScreen(loc);int left = loc[0], top = loc[1], right = left + view.getWidth(), bottom = top + view.getHeight();float x = event.getRawX(), y = event.getRawY();return x >= left && x <= right && y >= top && y <= bottom;}/*** 隐藏软键盘*/protected void hideSoftKeyboard(View view) {if (view == null) return;InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);if (imm != null) imm.hideSoftInputFromWindow(view.getWindowToken(), 0);}/*** 设置沉浸式状态栏(半透明)*/private void setImmersiveStatusBar(Window window) {window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);window.setStatusBarColor(Color.TRANSPARENT);}/*** 刘海屏适配*/private void setNotchScreen(Window window) {WindowManager.LayoutParams lp = window.getAttributes();lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;window.setAttributes(lp);}/*** 获取屏幕宽度*/protected int getScreenWidth() {return mContext.getResources().getDisplayMetrics().widthPixels;}/*** 获取屏幕高度*/protected int getScreenHeight() {return mContext.getResources().getDisplayMetrics().heightPixels;}/*** dp转px*/protected int dp2px(float dpValue) {return (int) (dpValue * mContext.getResources().getDisplayMetrics().density + 0.5f);}/*** 设置安全模式*/public void setSafeMode(boolean safeMode) {this.mSafeMode = safeMode;setCancelable(!safeMode);setCanceledOnTouchOutside(!safeMode);}/*** 设置倒计时自动关闭*/public void setAutoDismiss(int seconds, TextView countDownView) {this.mAutoDismissSeconds = seconds;this.mCountDownView = countDownView;}/*** 启动倒计时*/private void startCountDownTimer() {if (mAutoDismissSeconds > 0) {cancelCountDownTimer();mCountDownTimer = new CountDownTimer(mAutoDismissSeconds * 1000L, 1000L) {@Overridepublic void onTick(long millisUntilFinished) {int seconds = (int) (millisUntilFinished / 1000);if (mCountDownView != null) {mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));}}@Overridepublic void onFinish() {dismiss();}}.start();}}/*** 取消倒计时*/private void cancelCountDownTimer() {if (mCountDownTimer != null) {mCountDownTimer.cancel();mCountDownTimer = null;}}/*** 设置弹窗显示次数限制*/public void setShowCountLimit(String dialogId, int maxShowCount, long timeRangeMillis) {this.mDialogId = dialogId;this.mMaxShowCount = maxShowCount;this.mTimeRangeMillis = timeRangeMillis;}/*** 检查是否还能显示*/public boolean canShow() {if (mMaxShowCount < 0 || TextUtils.isEmpty(mDialogId)) {return true; // 没有设置限制}SharedPreferences sp = mContext.getSharedPreferences("dialog_show_count", Context.MODE_PRIVATE);long firstShowTime = sp.getLong(mDialogId + "_first_time", 0);int showCount = sp.getInt(mDialogId + "_count", 0);long currentTime = System.currentTimeMillis();// 如果超过时间范围,重置计数if (firstShowTime == 0 || currentTime - firstShowTime > mTimeRangeMillis) {sp.edit().putLong(mDialogId + "_first_time", currentTime).putInt(mDialogId + "_count", 1).apply();return true;}// 如果没超过次数限制if (showCount < mMaxShowCount) {sp.edit().putInt(mDialogId + "_count", showCount + 1).apply();return true;}return false; // 超过限制,不能显示}/*** 设置是否可拖拽*/public void setDraggable(boolean draggable) {this.mDraggable = draggable;setupDragListener();}/*** 设置拖拽区域视图*/public void setDragView(View dragView) {this.mDragView = dragView;setupDragListener();}/*** 设置是否启用磁吸效果*/public void setMagneticEffect(boolean magneticEffect) {this.mMagneticEffect = magneticEffect;}/*** 设置磁吸生效距离*/public void setMagneticRange(int range) {this.mMagneticRange = range;}/*** 设置是否保存位置*/public void setSavePosition(boolean savePosition, String positionTag) {this.mSavePosition = savePosition;this.mPositionTag = positionTag;}/*** 恢复保存的位置*/private void restoreSavedPosition() {if (!mSavePosition || TextUtils.isEmpty(mPositionTag)) return;SharedPreferences sp = mContext.getSharedPreferences("dialog_positions", Context.MODE_PRIVATE);int x = sp.getInt(mPositionTag + "_x", -1);int y = sp.getInt(mPositionTag + "_y", -1);if (x != -1 && y != -1) {Window window = getWindow();if (window != null) {WindowManager.LayoutParams params = window.getAttributes();params.x = x;params.y = y;window.setAttributes(params);}}}/*** 保存当前位置*/private void saveCurrentPosition() {if (!mSavePosition || TextUtils.isEmpty(mPositionTag)) return;Window window = getWindow();if (window != null) {WindowManager.LayoutParams params = window.getAttributes();SharedPreferences sp = mContext.getSharedPreferences("dialog_positions", Context.MODE_PRIVATE);sp.edit().putInt(mPositionTag + "_x", params.x).putInt(mPositionTag + "_y", params.y).apply();}}/*** 设置拖拽监听*/private void setupDragListener() {if (!mDraggable) return;View dragTarget = mDragView != null ? mDragView : getWindow().getDecorView();dragTarget.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {Window window = getWindow();if (window == null) return false;WindowManager.LayoutParams params = window.getAttributes();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:// 记录触摸开始位置mTouchStartX = event.getRawX();mTouchStartY = event.getRawY();mWindowStartX = params.x;mWindowStartY = params.y;return true;case MotionEvent.ACTION_MOVE:// 计算移动距离float dx = event.getRawX() - mTouchStartX;float dy = event.getRawY() - mTouchStartY;// 更新窗口位置params.x = (int) (mWindowStartX + dx);params.y = (int) (mWindowStartY + dy);// 限制弹窗在屏幕内params.x = Math.max(0, Math.min(params.x, mScreenWidth - getDialogWidth()));params.y = Math.max(0, Math.min(params.y, mScreenHeight - getDialogHeight()));window.setAttributes(params);return true;case MotionEvent.ACTION_UP:// 处理磁吸效果if (mMagneticEffect) {handleMagneticEffect(params);}// 保存位置saveCurrentPosition();return true;}return false;}});}/*** 处理磁吸效果*/private void handleMagneticEffect(WindowManager.LayoutParams params) {// 左边缘磁吸if (params.x <= mMagneticRange) {params.x = 0;}// 右边缘磁吸else if (params.x >= mScreenWidth - getDialogWidth() - mMagneticRange) {params.x = mScreenWidth - getDialogWidth();}// 上边缘磁吸if (params.y <= mMagneticRange) {params.y = 0;}// 下边缘磁吸else if (params.y >= mScreenHeight - getDialogHeight() - mMagneticRange) {params.y = mScreenHeight - getDialogHeight();}getWindow().setAttributes(params);}/*** 获取弹窗宽度*/private int getDialogWidth() {Window window = getWindow();if (window == null) return 0;return window.getDecorView().getWidth();}/*** 获取弹窗高度*/private int getDialogHeight() {Window window = getWindow();if (window == null) return 0;return window.getDecorView().getHeight();}@Overridepublic void show() {if (canShow()) {super.show();}}@Overridepublic void dismiss() {saveCurrentPosition(); // 关闭时保存位置cancelCountDownTimer(); // 关闭时取消倒计时super.dismiss();}// ========== 对外设置方法 ==========public void setGravity(int gravity) {this.mGravity = gravity;setWindowAttributes();}public void setAnimationStyle(int style) {this.mAnimationStyle = style;setWindowAttributes();}public void setFullScreen(boolean fullScreen) {this.mFullScreen = fullScreen;setWindowAttributes();}public void setImmersive(boolean immersive) {this.mImmersive = immersive;setWindowAttributes();}public void setNotchScreen(boolean notchScreen) {this.mNotchScreen = notchScreen;setWindowAttributes();}public void setKeyboardAdapt(boolean keyboardAdapt) {this.mKeyboardAdapt = keyboardAdapt;setWindowAttributes();}
}
使用示例
java
运行
public class MyDialog extends BaseDialog {public MyDialog(Context context) {super(context, R.layout.dialog_my);}@Overrideprotected void initView() {// 设置可拖拽setDraggable(true);// 设置磁吸效果setMagneticEffect(true);// 设置位置保存setSavePosition(true, "my_dialog");findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());}@Overrideprotected void initData() {}
}// 调用
MyDialog dialog = new MyDialog(this);
dialog.show();
🚀 功能亮点
✅ 拖拽功能:支持整个弹窗或指定区域拖拽✅ 磁吸效果:靠近屏幕边缘自动吸附✅ 位置保存:下次打开自动恢复上次位置✅ 安全模式:防止误触关闭✅ 倒计时关闭:自动消失功能✅ 次数限制:控制弹窗显示频率✅ 完整适配:包括沉浸式、刘海屏、软键盘等✅ 动画支持:自定义入场退场动画