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();

🚀 功能亮点

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

相关推荐
sun0077003 小时前
OverlayManager service, overlay, idmap, rro, android
android
_Sem4 小时前
Compose 动画 + KMM 跨平台开发:从传统View到现代声明式UI动画
android·composer
2501_916007475 小时前
前端开发工具都有哪些?常用前端开发工具清单与场景化推荐
android·ios·小程序·https·uni-app·iphone·webview
2501_915909068 小时前
iOS 应用上架全流程解析,苹果应用发布步骤、ipa 上传工具、TestFlight 测试与 App Store 审核经验
android·macos·ios·小程序·uni-app·cocoa·iphone
路上^_^8 小时前
安卓基础组件024-fagment
android
ljt27249606618 小时前
Compose笔记(五十一)--rememberTextMeasurer
android·笔记·android jetpack
阿蓝8589 小时前
Android代码架构
android
ZFJ_张福杰15 小时前
【Flutter】GetX最佳实践与避坑指南
android·flutter·ios·getx
一直向钱1 天前
android 基于okhttp的socket封装
android·okhttp