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;
    }

    @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。
  • 如果你的弹窗是全屏的,建议用 DialogBottomAnimDialogTopAnim,效果更像原生的 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 中加一个弹窗显示次数限制 ,并且支持同一个时间范围内同一个弹窗的限制。

一、功能说明

我会帮你实现:

  1. 同一个弹窗类型 :用唯一 ID(比如 dialog_privacydialog_update)来区分不同弹窗。
  2. 时间范围限制:比如一天、三天、一周内最多显示几次。
  3. 次数限制:在指定时间范围内,达到次数后不再显示。
  4. 数据持久化 :用 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();

🚀 功能亮点

拖拽功能 :支持整个弹窗或指定区域拖拽✅ 磁吸效果 :靠近屏幕边缘自动吸附✅ 位置保存 :下次打开自动恢复上次位置✅ 安全模式 :防止误触关闭✅ 倒计时关闭 :自动消失功能✅ 次数限制 :控制弹窗显示频率✅ 完整适配 :包括沉浸式、刘海屏、软键盘等✅ 动画支持:自定义入场退场动画

相关推荐
恋猫de小郭5 分钟前
Flutter 3.38 发布,快来看看有什么更新吧
android·前端·flutter
百锦再5 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子6 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师7 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
江上清风山间明月9 小时前
Android 系统超级实用的分析调试命令
android·内存·调试·dumpsys
百锦再10 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
用户693717500138413 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
SHEN_ZIYUAN14 小时前
Android 主线程性能优化实战:从 90% 降至 13%
android·cpu优化
曹绍华14 小时前
android 线程loop
android·java·开发语言
雨白14 小时前
Hilt 入门指南:从 DI 原理到核心用法
android·android jetpack