代码如下
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;
}
@Override
protected 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();
}
@Override
public void setCancelable(boolean flag) {
super.setCancelable(flag);
this.mCancelable = flag;
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
this.mCanceledOnTouchOutside = cancel;
}
@Override
public <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" />
<!-- 边框(可选) -->
<stroke
android: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);
}
@Override
protected void initView() {
findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());
}
@Override
protected 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;
}
@Override
protected 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();
}
@Override
public void setCancelable(boolean flag) {
super.setCancelable(flag);
this.mCancelable = flag;
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
this.mCanceledOnTouchOutside = cancel;
}
@Override
public <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;
}
@Override
protected 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) {
@Override
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
if (mCountDownView != null) {
mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));
}
}
@Override
public void onFinish() {
dismiss();
}
}.start();
}
}
/**
* 取消倒计时
*/
private void cancelCountDownTimer() {
if (mCountDownTimer != null) {
mCountDownTimer.cancel();
mCountDownTimer = null;
}
}
@Override
public 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();
}
@Override
public void setCancelable(boolean flag) {
super.setCancelable(flag);
this.mCancelable = flag;
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
this.mCanceledOnTouchOutside = cancel;
}
@Override
public <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);
}
@Override
protected void initView() {
TextView tvCountDown = findViewById(R.id.tv_count_down);
// 设置5秒后自动关闭,并更新倒计时文本
setAutoDismiss(5, tvCountDown);
findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());
}
@Override
protected 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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个自动关闭的弹窗"
android:textColor="@color/black"
android:textSize="18sp" />
<TextView
android: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" />
<Button
android: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; // 当前弹窗唯一ID
private 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;
}
@Override
protected 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) {
@Override
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
if (mCountDownView != null) {
mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));
}
}
@Override
public 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; // 超过限制,不能显示
}
@Override
public void show() {
if (canShow()) {
super.show();
}
}
@Override
public 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();
}
@Override
public void setCancelable(boolean flag) {
super.setCancelable(flag);
this.mCancelable = flag;
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
this.mCanceledOnTouchOutside = cancel;
}
@Override
public <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; // 当前弹窗唯一ID
private 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();
}
@Override
protected 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) {
@Override
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
if (mCountDownView != null) {
mCountDownView.setText(String.format("将在 %d 秒后自动关闭", seconds));
}
}
@Override
public 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() {
@Override
public 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();
}
@Override
public void show() {
if (canShow()) {
super.show();
}
}
@Override
public 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);
}
@Override
protected void initView() {
// 设置可拖拽
setDraggable(true);
// 设置磁吸效果
setMagneticEffect(true);
// 设置位置保存
setSavePosition(true, "my_dialog");
findViewById(R.id.btn_close).setOnClickListener(v -> dismiss());
}
@Override
protected void initData() {}
}
// 调用
MyDialog dialog = new MyDialog(this);
dialog.show();
🚀 功能亮点
✅ 拖拽功能 :支持整个弹窗或指定区域拖拽✅ 磁吸效果 :靠近屏幕边缘自动吸附✅ 位置保存 :下次打开自动恢复上次位置✅ 安全模式 :防止误触关闭✅ 倒计时关闭 :自动消失功能✅ 次数限制 :控制弹窗显示频率✅ 完整适配 :包括沉浸式、刘海屏、软键盘等✅ 动画支持:自定义入场退场动画