Android 项目中 BaseActivity 封装实践(支持 ViewBinding、PermissionUtils动态权限、加载弹窗和跳转动画)

在大型 Android 项目中,Activity 的重复逻辑非常多,例如:

  • 加载弹窗
  • 权限请求
  • 用户登录检查
  • 页面跳转动画
  • EventBus 事件管理

为了减少重复代码,提高可维护性,我们可以封装一个 BaseActivity 来统一管理这些功能。本文将结合实际代码讲解封装思路。

一. BaseActivity 核心功能

1.1 支持 ViewBinding 和普通布局

BaseActivity 允许子类通过两种方式初始化布局:

bash 复制代码
// 使用布局ID
setContentView(getLayoutResourceId());

// 使用 ViewBinding
mViewBinding = initViewBinding();
setContentView(mViewBinding.getRoot());

子类只需实现:

bash 复制代码
protected abstract int getLayoutResourceId();
protected abstract VB initViewBinding();

这样可以兼容不同页面风格。

1.2 权限请求封装

使用 ActivityResultLauncher 统一处理权限请求,并自动弹出提示 Dialog:

bash 复制代码
protected void requestPermissionsCompat(@NonNull String[] permissions,
                                        @NonNull PermissionUtils.PermissionCallback callback) {
    List<String> toRequest = new ArrayList<>();
    for (String perm : permissions) {
        if (!PermissionUtils.isPermissionGranted(this, perm)) {
            toRequest.add(perm);
        }
    }

    if (toRequest.isEmpty()) {
        callback.onGranted();
    } else {
        // 显示权限告知弹窗
        Pair<String, String> desc = PermissionUtils.getUnifiedPermissionDescription(toRequest);
        showPermissionTipDialog(desc.first, desc.second);
        this.permissionCallback = callback;
        permissionLauncher.launch(toRequest.toArray(new String[0]));
    }
}
  • 自动判断权限是否被永久拒绝
  • 提供告知和拒绝提示 Dialog
  • 支持回调 onGranted() 和 onDenied()

1.3 加载弹窗封装

BaseActivity 提供统一的加载弹窗管理:

bash 复制代码
public void showLoading(String message) {
    if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
        mLoadingDialog.setMessage(message);
        mLoadingDialog.show();
    }
}

public void forceDismissLoading() {
    if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
        mLoadingDialog.forceDismiss();
    }
}
  • 支持延迟关闭
  • 支持立即关闭
  • 弹窗和业务逻辑解耦

1.4 Activity 跳转与返回结果

封装 startIntent 和 startIntentForResult,统一处理动画和返回值:

bash 复制代码
public void startIntentForResult(Class<?> targetClass, ActivityResultCallback<ActivityResult> callback) {
    this.activityResultCallback = callback;
    Intent intent = new Intent(this, targetClass);
    activityResultLauncher.launch(intent);
    applyStartTransitionAnim();
}

@Override
public void finish() {
    super.finish();
    applyFinishTransitionAnim();
}
  • 自动管理跳转动画
  • 支持清空 Activity 栈
  • 支持返回数据封装

1.5 沉浸式状态栏与全屏模式

兼容不同 Android 版本,实现沉浸式状态栏,并可设置状态栏文字颜色:

bash 复制代码
public void setStatusBarTextColor(boolean dark) {
    Window window = getWindow();
    View decorView = window.getDecorView();
    window.setStatusBarColor(Color.TRANSPARENT);
    window.setNavigationBarColor(Color.TRANSPARENT);

    WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(window, decorView);
    controller.setAppearanceLightStatusBars(dark);
    controller.setAppearanceLightNavigationBars(dark);

    int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

    decorView.setSystemUiVisibility(flags);
}
  • 内容可延伸到状态栏/导航栏
  • 支持粘性沉浸
  • 可动态切换文字颜色

1.6 EventBus 统一注册和销毁

在 onCreate 注册,在 onDestroy 注销,避免重复注册或内存泄漏:

bash 复制代码
EventBusUtils.register(this);
EventBusUtils.unregister(this);

二. BaseActivity代码示例

bash 复制代码
package com.xxxx.xxxx.base;

import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.viewbinding.ViewBinding;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.account_operation.UserLoginActivity;
import com.xxxx.xxxx.bean.EventBusBean;
import com.xxxx.xxxx.bean.UserInfoBean;
import com.xxxx.xxxx.dialog.AdvancedLoadingDialog;
import com.xxxx.xxxx.dialog.PermissionDeniedTipDialog;
import com.xxxx.xxxx.dialog.PermissionTipDialog;
import com.xxxx.xxxx.utils.ActivityCollector;
import com.xxxx.xxxx.utils.AppInfoUtils;
import com.xxxx.xxxx.utils.AuthManager;
import com.xxxx.xxxx.utils.EnumUtils;
import com.xxxx.xxxx.utils.EventBusUtils;
import com.xxxx.xxxx.utils.EventListener;
import com.xxxx.xxxx.utils.PermissionUtils;
import com.xxxx.xxxx.utils.StringUtils;

import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: Su
 * @Date: 2024/2/17
 * @Description: 统一管理 Activity 生命周期,提供公共方法,优化 EventBus、状态栏、Activity 退出等逻辑
 */
public abstract class BaseActivity<VB extends ViewBinding> extends AppCompatActivity {

    protected VB mViewBinding;

    protected final ExecutorService executor = Executors.newSingleThreadExecutor();
    protected final Handler mainHandler = new Handler(Looper.getMainLooper());

    // 当前 Activity 的弱引用,避免持有强引用导致泄漏
    private static WeakReference<BaseActivity> currentActivity;

    // 是否启用跳转动画(默认开启)
    protected boolean enableTransitionAnim = true;

    public Intent mIntent = null;

    // 加载弹窗
    private AdvancedLoadingDialog mLoadingDialog;
    //用户Token
    protected String mUserToken = "";
    //用户ID
    protected String mUserId = "";
    //用户信息
    protected UserInfoBean mUserInfoBean;

    // Activity 结果启动器
    private ActivityResultLauncher<Intent> activityResultLauncher;

    // Activity 结果回调接口
    private ActivityResultCallback<ActivityResult> activityResultCallback;

    // Android 13 及以上版本的返回回调
    private OnBackInvokedCallback backInvokedCallback;

    // Android 12 及以下版本的返回回调
    private OnBackPressedCallback backPressedCallback;

    // 权限申请
    private ActivityResultLauncher<String[]> permissionLauncher;
    private PermissionUtils.PermissionCallback permissionCallback;
    //权限告知弹窗
    private PermissionTipDialog mPermissionTipDialog = null;
    //权限拒绝告知弹窗
    private PermissionDeniedTipDialog mPermissionDeniedTipDialog = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 判断是否使用 ViewBinding 初始化视图
        if (getLayoutResourceId() == 0) {
            if (initViewBinding() != null) {
                mViewBinding = initViewBinding();
                setContentView(mViewBinding.getRoot());
            }
        } else {
            setContentView(getLayoutResourceId());
        }

        //设置当前 Activity 的屏幕方向
//        setScreenOrientation(isPortraitOnly());

        // 将当前 Activity 添加到集合中(使用弱引用)
        ActivityCollector.addActivity(this);
        currentActivity = new WeakReference<>(this);

        // 初始化 UI 组件
        initViews(savedInstanceState);

        // 验证登录是否失效
        initLoginToken();
        // 更新用户信息
        updateUserInFo();

        //初始化弹窗
        initDialog();
        initLoadingDialog();
        initPermissionTipDialog();
        // 设置监听事件
        initListeners();
        // 初始化数据
        initData();

        // 注册 EventBus,避免重复注册
        EventBusUtils.register(this);

        // 初始化 Activity 结果启动器
        initActivityResult();

        // 注册权限请求结果处理
        initPermissionLauncher();

    }

    /**
     * @return 获取当前 Activity 的布局 ID,子类必须实现
     */
    protected abstract int getLayoutResourceId();

    /**
     * 返回是否使用 ViewBinding。子类可以根据需求覆盖该方法。
     * 默认返回 false,表示不使用 ViewBinding。
     */
    protected boolean useViewBinding() {
        return false;
    }

    /**
     * 当使用 ViewBinding 时,子类必须实现该方法来返回对应的 ViewBinding 实例。
     */
    protected abstract VB initViewBinding();

    /**
     * 初始化 UI 组件,子类必须实现
     */
    protected abstract void initViews(Bundle savedInstanceState);

    /**
     * 设置监听事件,子类必须实现
     */
    protected abstract void initListeners();

    /**
     * 初始化数据,子类必须实现
     */
    protected abstract void initData();


    /**
     * 初始化弹窗,子类必须实现
     */
    protected abstract void initDialog();

    /**
     * 设置当前 Activity 的屏幕方向
     *
     * @param portraitOnly 是否强制竖屏
     *                     true  → 强制竖屏(常见页面:列表、详情等)
     *                     false → 允许横屏(常见页面:视频播放、地图等)
     *
     * 使用示例:
     *   setScreenOrientation(true);   // 当前页面强制竖屏
     *   setScreenOrientation(false);  // 当前页面允许横屏
     */
    protected void setScreenOrientation(boolean portraitOnly) {
        if (portraitOnly) {
            // 强制竖屏
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        } else {
            // 允许横屏(用户旋转时,自动横屏)
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
        }
    }

    protected boolean isPortraitOnly() {
        return true; // 默认强制竖屏
    }

    @Override
    protected void onResume() {
        super.onResume();
        currentActivity = new WeakReference<>(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        currentActivity = null;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        forceDismissLoading();

        dismissPermissionTipDialog();

        dismissPermissionDeniedTipDialog();

        // 取消 EventBus 注册(防止未注册时取消导致异常)
        EventBusUtils.unregister(this);

        // 移除当前 Activity,避免内存泄漏
        ActivityCollector.removeActivity(this);

        executor.shutdown(); // 释放线程池资源
    }

    /**
     * 获取当前 Activity
     *
     * @return 当前正在运行的 Activity
     */
    public static BaseActivity getCurrentActivity() {
        return currentActivity != null ? currentActivity.get() : null;
    }

    /**
     * 显示 Toast 消息
     *
     * @param message 需要显示的文本
     */
    public void showToast(String message) {
        runOnUiThread(() -> Toast.makeText(this, message, Toast.LENGTH_SHORT).show());
    }

    /**
     * 启动新的 Activity,并传递数据
     *
     * @param targetClass 目标 Activity 类
     * @param data        要传递的数据 Bundle
     * @param clearStack  是否清除当前 Activity 栈
     */
    public void startIntent(Class<?> targetClass, Bundle data, boolean clearStack) {
        Intent intent = new Intent(this, targetClass);
        if (clearStack) {
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        if (data != null) {
            intent.putExtras(data);
        }
        startActivity(intent);
    }

    /**
     * 启动新的 Activity,不清除当前栈
     *
     * @param targetClass 目标 Activity 类
     * @param data        要传递的数据 Bundle
     */
    public void startIntent(Class<?> targetClass, Bundle data) {
        startIntent(targetClass, data, false);
    }

    /**
     * 启动新的 Activity,并且可选择是否清除当前 Activity 栈
     *
     * @param targetClass 目标 Activity 类
     * @param clearStack  是否清除栈中的当前 Activity
     */
    public void startIntent(Class<?> targetClass, boolean clearStack) {

        // 先启动登录页
        Intent intent = new Intent(this, targetClass);
        if (clearStack) {
            // 注意:不能加 CLEAR_TASK、NEW_TASK,否则必闪启动图
            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

            startActivity(intent);
            // 关闭所有旧页面,但保留刚启动的登录页
            ActivityCollector.finishAllExcept(targetClass);
        }else {

            startActivity(intent);
        }

    }

    /**
     * 启动新的 Activity,并且销毁当前 Activity
     *
     * @param targetClass 目标 Activity 类
     */
    public void startIntentFinish(Class<?> targetClass) {
        startIntent(targetClass);

        finish();
    }

    /**
     * 启动新的 Activity,不清除当前栈
     *
     * @param targetClass 目标 Activity 类
     */
    public void startIntent(Class<?> targetClass) {
        startIntent(targetClass, false);
    }

    /**
     * 初始化 Activity 结果启动器,并注册结果回调。
     */
    private void initActivityResult() {
        activityResultLauncher = registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (activityResultCallback != null) {
                        activityResultCallback.onActivityResult(result);
                    }
                }
        );
    }

    /**
     * 启动新的 Activity,并等待结果回调。
     *
     * @param targetClass 目标 Activity 类
     * @param data        要传递的数据 Bundle
     * @param callback    结果回调接口
     */
    public void startIntentForResult(Class<?> targetClass, Bundle data, ActivityResultCallback<ActivityResult> callback) {
        this.activityResultCallback = callback;
        Intent intent = new Intent(this, targetClass);
        if (data != null) {
            intent.putExtras(data);
        }
        activityResultLauncher.launch(intent);

        // 这里手动调用动画
        applyStartTransitionAnim();
    }

    /**
     * 启动新的 Activity,并等待结果回调。
     *
     * @param targetClass 目标 Activity 类
     * @param callback    结果回调接口
     */
    public void startIntentForResult(Class<?> targetClass, ActivityResultCallback<ActivityResult> callback) {
        this.activityResultCallback = callback;
        Intent intent = new Intent(this, targetClass);
        activityResultLauncher.launch(intent);

        // 这里手动调用动画
        applyStartTransitionAnim();
    }

    /**
     * 设置返回结果并关闭当前 Activity。
     *
     * @param resultCode 返回结果码,例如 Activity.RESULT_OK
     * @param data       要返回的数据,可以为 null
     */
    protected void setResultAndFinish(int resultCode, @Nullable Intent data) {
        setResult(resultCode, data);
        finish();
    }

    /**
     * 设置返回结果并关闭当前 Activity。
     *
     * @param resultCode 返回结果码,例如 Activity.RESULT_OK
     * @param key        返回数据的键
     * @param value      返回数据的值
     */
    protected void setResultAndFinish(int resultCode, @NonNull String key, @NonNull String value) {
        Intent data = new Intent();
        data.putExtra(key, value);
        setResult(resultCode, data);
        finish();
    }

    /**
     * 设置返回结果并关闭当前 Activity。
     *
     * @param resultCode 返回结果码,例如 Activity.RESULT_OK
     * @param key        返回数据的键
     * @param value      返回数据的值
     */
    protected void setResultAndFinish(int resultCode, @NonNull String key, @NonNull boolean value) {
        Intent data = new Intent();
        data.putExtra(key, value);
        setResult(resultCode, data);
        finish();
    }

    /**
     * 设置返回结果并关闭当前 Activity。
     *
     * @param resultCode 返回结果码,例如 Activity.RESULT_OK
     * @param key        返回数据的键
     * @param value      返回数据的值
     */
    protected void setResultAndFinish(int resultCode, @NonNull String key, @NonNull boolean value, @NonNull String key1, @NonNull boolean value1) {
        Intent data = new Intent();
        data.putExtra(key, value);
        data.putExtra(key1, value1);
        setResult(resultCode, data);
        finish();
    }

    /**
     * 设置返回结果并关闭当前 Activity。
     *
     * @param resultCode 返回结果码,例如 Activity.RESULT_OK
     * @param key        返回数据的键
     * @param value      返回数据的值
     */
    protected void setResultAndFinish(int resultCode, @NonNull String key, @NonNull String value, @NonNull String key1, @NonNull boolean value1) {
        Intent data = new Intent();
        data.putExtra(key, value);
        data.putExtra(key1, value1);
        setResult(resultCode, data);
        finish();
    }

    @Override
    public void startActivity(Intent intent) {
        super.startActivity(intent);
        applyStartTransitionAnim();
    }

    @Override
    public void finish() {
        super.finish();
        applyFinishTransitionAnim();
    }

    /**
     * 设置是否启用页面跳转动画
     */
    public void setEnableTransitionAnim(boolean enable) {
        this.enableTransitionAnim = enable;
    }

    /**
     * 页面进入动画(可自定义为高级动效)
     */
    protected void applyStartTransitionAnim() {
        if (enableTransitionAnim) {
            overridePendingTransition(R.anim.activity_enter_anim, R.anim.activity_exit_anim);
        }
    }

    /**
     * 页面退出动画(可自定义为高级动效)
     */
    protected void applyFinishTransitionAnim() {
        if (enableTransitionAnim) {
            overridePendingTransition(R.anim.activity_enter_anim, R.anim.activity_exit_anim);
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventTouch(EventBusBean event) {

    }

    /**
     * 初始化权限告知弹窗
     */
    private void initPermissionTipDialog() {
        mPermissionTipDialog = new PermissionTipDialog(this);

        mPermissionDeniedTipDialog = new PermissionDeniedTipDialog(this);
        mPermissionDeniedTipDialog.setEventListener(new EventListener<String>() {
            @Override
            public void onAction(EnumUtils.EventListenerAction action) {
                super.onAction(action);
                AppInfoUtils.openAppSettings(getApplicationContext());
            }
        });
    }


    /**
     * 显示权限告知对话框
     *
     * @param permissionName 权限名称
     * @param permissionContent 权限描述
     */
    public void showPermissionTipDialog(String permissionName, String permissionContent) {
        if (mPermissionTipDialog != null && !mPermissionTipDialog.isShowing()) {
            mPermissionTipDialog.show();
            mPermissionTipDialog.setData(permissionName, permissionContent);
        }
    }

    /**
     * 显示权限拒绝告知对话框
     *
     * @param permissionName 权限名称
     * @param permissionContent 权限描述
     */
    public void showPermissionDeniedTipDialog(String permissionName, String permissionContent) {
        if (mPermissionDeniedTipDialog != null && !mPermissionDeniedTipDialog.isShowing()) {
            mPermissionDeniedTipDialog.show();
            mPermissionDeniedTipDialog.setData(permissionName, permissionContent);
        }
    }

    /**
     * 关闭权限告知对话框
     */
    public void dismissPermissionTipDialog() {
        if (mPermissionTipDialog != null && mPermissionTipDialog.isShowing()) {
            mPermissionTipDialog.dismiss();
        }
    }

    /**
     * 关闭权限拒绝告知对话框
     */
    public void dismissPermissionDeniedTipDialog() {
        if (mPermissionDeniedTipDialog != null && mPermissionDeniedTipDialog.isShowing()) {
            mPermissionDeniedTipDialog.dismiss();
        }
    }

    /**
     * 初始化加载弹窗
     */
    private void initLoadingDialog() {
        mLoadingDialog = new AdvancedLoadingDialog(this);
    }

    /**
     * 显示加载对话框
     *
     * @param message 要显示的提示信息
     */
    public void showLoading(String message) {
        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
            mLoadingDialog.setMessage(message);
            mLoadingDialog.show();
        }
    }

    public void showLoading() {
        if (mLoadingDialog != null && !mLoadingDialog.isShowing()) {
            mLoadingDialog.show();
        }
    }

    /**
     * 延迟关闭加载对话框
     */
    public void delayDismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.delayedDismiss();
        }
    }

    /**
     * 强制立即关闭加载对话框
     */
    public void forceDismissLoading() {
        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
            mLoadingDialog.forceDismiss();
        }
    }

    /**
     * 初始化登录令牌,检查用户登录状态。
     * 如果用户未登录,并且当前 Activity 不是排除的 Activity,则跳转到登录页面。
     */
    private void initLoginToken() {
        mUserToken = AuthManager.getUserToken();
        // 检查当前 Activity 是否为排除的 Activity
        if (StringUtils.isBlank(mUserToken) && getCurrentActivity() != null && !ActivityCollector.isExcludedActivity(getCurrentActivity().getClass())) {
            // 跳转到登录页面,清除当前 Activity 栈
            startIntent(UserLoginActivity.class, true);
        }
    }

    /**
     * 更新用户信息
     */
    public void updateUserInFo() {
        mUserId = AuthManager.getUserId();
        mUserInfoBean = AuthManager.getUserInfo();
    }

    /**
     * 设置返回键处理逻辑,兼容不同 Android 版本。
     */
    public void setupBackHandler() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Android 13 及以上版本
            backInvokedCallback = this::onBackPressedCustom;
            getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                    OnBackInvokedDispatcher.PRIORITY_DEFAULT,
                    backInvokedCallback
            );
        } else {
            // Android 12 及以下版本
            backPressedCallback = new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    onBackPressedCustom();
                }
            };
            getOnBackPressedDispatcher().addCallback(this, backPressedCallback);
        }
    }

    public void unregisterBack() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && backInvokedCallback != null) {
            getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(backInvokedCallback);
        }
    }

    /**
     * 自定义的返回键处理方法,子类可根据需要重写。
     */
    protected void onBackPressedCustom() {
        // 默认行为:关闭当前 Activity

    }

    // 注册权限请求结果处理
    private void initPermissionLauncher() {
        permissionLauncher = registerForActivityResult(
                new ActivityResultContracts.RequestMultiplePermissions(),
                result -> {
                    List<String> deniedList = new ArrayList<>();
                    boolean permanentDenied = false;

                    for (Map.Entry<String, Boolean> entry : result.entrySet()) {
                        if (!entry.getValue()) {
                            deniedList.add(entry.getKey());
                            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, entry.getKey())) {
                                permanentDenied = true;
                            }
                        }
                    }

                    if (permissionCallback != null) {
                        if (deniedList.isEmpty()) {
                            permissionCallback.onGranted();
                        } else {
                            //显示权限告知弹窗
                            Pair<String, String> desc = PermissionUtils.getUnifiedPermissionDescription(deniedList);
                            showPermissionDeniedTipDialog(desc.first, desc.second);
                            permissionCallback.onDenied(deniedList, permanentDenied);
                        }
                    }
                    //关闭权限告知弹窗
                    dismissPermissionTipDialog();

                }
        );
    }

    /**
     * 请求权限(推荐在子类中调用)
     */
    protected void requestPermissionsCompat(@NonNull String[] permissions,
                                            @NonNull PermissionUtils.PermissionCallback callback) {
        List<String> toRequest = new ArrayList<>();
        for (String perm : permissions) {
            if (!PermissionUtils.isPermissionGranted(this, perm)) {
                toRequest.add(perm);
            }
        }

        if (toRequest.isEmpty()) {
            callback.onGranted();
        } else {
            //显示权限告知弹窗
            Pair<String, String> desc = PermissionUtils.getUnifiedPermissionDescription(toRequest);
            showPermissionTipDialog(desc.first, desc.second);
            this.permissionCallback = callback;
            permissionLauncher.launch(toRequest.toArray(new String[0]));
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
//            setupImmersiveMode();
        }
    }

    /**
     * 设置沉浸式模式,使内容可以延伸到系统栏(状态栏/导航栏)区域
     * 同时保持系统栏自动隐藏的特性
     */
    private void setupImmersiveMode() {
        // 获取窗口的根DecorView
        View decorView = getWindow().getDecorView();

        // 设置系统UI可见性标志组合
        decorView.setSystemUiVisibility(
                // 保持布局稳定 - 当系统UI可见性变化时,不调整布局边距
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE

                        // 允许内容延伸到导航栏区域 - 不预留导航栏空间
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

                        // 允许内容延伸到状态栏区域 - 不预留状态栏空间
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

                        // 隐藏导航栏 - 但不预留空间(与LAYOUT_HIDE_NAVIGATION配合)
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                        /*
                         * 【注意】不要使用SYSTEM_UI_FLAG_FULLSCREEN
                         * 因为它会强制为状态栏保留空间,导致顶部出现黑条
                         * 我们使用LAYOUT_FULLSCREEN+透明状态栏来实现真正的全屏
                         */
                        // | View.SYSTEM_UI_FLAG_FULLSCREEN

                        // 沉浸式粘性模式 - 系统UI临时显示后自动隐藏
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
        );

        /**
         * 设置WindowInsets监听 - 处理系统栏区域的变化
         *
         * 这个监听器可以:
         * 1. 获取系统栏(状态栏/导航栏)的尺寸
         * 2. 在系统栏显示/隐藏时调整布局
         * 3. 处理刘海屏、挖孔屏等特殊区域
         *
         * 注意:如果不需要精确控制内容布局,可以保留空实现
         * 但必须设置监听器才能使LAYOUT标志生效
         */
        decorView.setOnApplyWindowInsetsListener((v, insets) -> {
            // 默认实现:直接返回insets
            // 如果需要调整布局,可以在这里处理:
            // 例如:v.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
            return insets;
        });

        /**
         * 补充说明:
         * 1. 需要在Activity的onCreate中调用
         * 2. 需要配合以下主题设置:
         *    <item name="android:windowTranslucentStatus">true</item>
         *    <item name="android:windowTranslucentNavigation">true</item>
         * 3. 需要设置状态栏颜色透明:
         *    window.setStatusBarColor(Color.TRANSPARENT);
         */
    }


    /**
     * 设置沉浸式模式,同时控制状态栏文字颜色
     *
     * @param dark true = 黑色文字(适合浅色背景)
     *             false = 白色文字(适合深色背景)
     */
    public void setStatusBarTextColor(boolean dark) {
        Window window = getWindow();
        View decorView = window.getDecorView();

        // 1. 设置状态栏和导航栏透明
        window.setStatusBarColor(Color.TRANSPARENT);
        window.setNavigationBarColor(Color.TRANSPARENT);

        // 2. 使用 WindowInsetsControllerCompat 控制文字颜色
        WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(window, decorView);
        controller.setAppearanceLightStatusBars(dark); // 状态栏文字
        controller.setAppearanceLightNavigationBars(dark); // 导航栏图标

        // 3. 设置系统UI标志,实现内容延伸 + 隐藏导航栏 + 粘性沉浸
        int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;

        decorView.setSystemUiVisibility(flags);

        // 4. 设置 WindowInsetsListener 保证布局正确
        decorView.setOnApplyWindowInsetsListener((v, insets) -> {
            // 如果需要,可以根据 insets 调整 padding 或 margin
            // 例如:v.setPadding(0, insets.getSystemWindowInsetTop(), 0, insets.getSystemWindowInsetBottom());
            return insets;
        });
    }

}
UserLoginActivity

UserLoginActivity:登录页面

EventBusBean

EventBusBean:EventBus Bean类

bash 复制代码
public class EventBusBean {
    private String tag;
    private Object object;

    public EventBusBean(Object object, String tag) {
        this.tag = tag;
        this.object = object;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public String getTag() {
        return tag;
    }

    public Object getObject() {
        return object;
    }
}
AdvancedLoadingDialog

AdvancedLoadingDialog:加载弹窗 具体参考这篇文章:Android 通用 BaseDialog 实现:支持 ViewBinding + 全屏布局 + 加载弹窗

PermissionDeniedTipDialog

PermissionDeniedTipDialog:权限拒绝告知弹窗,更多请具体参考这篇文章:Android 通用 BaseDialog 实现:支持 ViewBinding + 全屏布局 + 加载弹窗

bash 复制代码
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.BaseDialog;
import com.xxxx.xxxx.base.MyApplication;
import com.xxxx.xxxx.databinding.DialogPermissionDeniedTipBinding;
import com.xxxx.xxxx.utils.EnumUtils;
import com.xxxx.xxxx.utils.EventListener;

/**
 * Author: Su
 * Date: 2025/12/20
 * Description: Description
 */

public class PermissionDeniedTipDialog extends BaseDialog<DialogPermissionDeniedTipBinding> implements View.OnClickListener {

    private EventListener<String> eventListener = null;

    public PermissionDeniedTipDialog(@NonNull Context context) {
        super(context, R.style.BaseDialogStyle);
    }

    public EventListener<String> getEventListener() {
        return eventListener;
    }

    public void setEventListener(EventListener<String> eventListener) {
        this.eventListener = eventListener;
    }

    @Override
    protected int getLayoutId() {
        return 0;
    }

    @Override
    protected DialogPermissionDeniedTipBinding initViewBinding() {
        return DialogPermissionDeniedTipBinding.inflate(getLayoutInflater());
    }

    @Override
    protected void initialize() {
        setWidth((int) MyApplication.getFloatDimension(R.dimen.dimens_270dp));
    }

    @Override
    protected void initListener() {
        mViewBinding.tvLayout4.setOnClickListener(this);
        mViewBinding.tvLayout5.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        int idTag = v.getId();

        if(idTag == R.id.tv_layout4) {
            if(eventListener != null) {
                eventListener.onAction(EnumUtils.EventListenerAction.CONFIRM);
            }
            dismiss();
        } else if(idTag == R.id.tv_layout5) {
            dismiss();
        }
    }

    public void setData(String permissionName, String permissionContent) {
        mViewBinding.tvLayout1.setText(permissionName);
        mViewBinding.tvLayout2.setText(permissionContent);
    }
}
bash 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/dimens_270dp"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:background="@drawable/bg_radius8_color1">

    <TextView
        android:id="@+id/tv_layout1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/none"
        android:textSize="@dimen/dimens_16dp"
        android:textColor="@color/dialog_permission_color1"
        android:textStyle="bold"
        android:layout_marginTop="@dimen/dimens_18dp"
        android:paddingLeft="@dimen/dimens_10dp"
        android:paddingRight="@dimen/dimens_10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <TextView
        android:id="@+id/tv_layout2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/none"
        android:textSize="@dimen/dimens_14dp"
        android:textColor="@color/dialog_permission_color3"
        android:layout_marginTop="@dimen/dimens_10dp"
        android:paddingLeft="@dimen/dimens_16dp"
        android:paddingRight="@dimen/dimens_16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <TextView
        android:id="@+id/tv_layout3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/dialog_permission_content1"
        android:textSize="@dimen/dimens_12dp"
        android:textColor="@color/dialog_permission_color4"
        android:layout_marginTop="@dimen/dimens_10dp"
        android:layout_marginLeft="@dimen/dimens_16dp"
        android:layout_marginRight="@dimen/dimens_16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout2"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <TextView
        android:id="@+id/tv_layout4"
        android:layout_width="0dp"
        android:layout_height="@dimen/dimens_36dp"
        android:text="@string/dialog_permission_content14"
        android:textColor="@color/dialog_permission_color5"
        android:textSize="@dimen/dimens_16dp"
        android:background="@drawable/bg_radius20_color1"
        android:gravity="center"
        android:layout_marginLeft="@dimen/dimens_16dp"
        android:layout_marginRight="@dimen/dimens_16dp"
        android:layout_marginTop="@dimen/dimens_20dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <TextView
        android:id="@+id/tv_layout5"
        android:layout_width="0dp"
        android:layout_height="@dimen/dimens_36dp"
        android:text="@string/dialog_permission_content15"
        android:textColor="@color/dialog_permission_color6"
        android:textSize="@dimen/dimens_16dp"
        android:gravity="center"
        android:layout_marginLeft="@dimen/dimens_16dp"
        android:layout_marginRight="@dimen/dimens_16dp"
        android:layout_marginTop="@dimen/dimens_10dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout4"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/dimens_16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout5"
        app:layout_constraintLeft_toLeftOf="parent"></View>

</androidx.constraintlayout.widget.ConstraintLayout>

drawable/bg_radius8_color1

bash 复制代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="@dimen/dimens_8dp"/>
    <solid android:color="@color/dialog_permission_color8" />
</shape>

drawable/bg_radius20_color1

bash 复制代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="@dimen/dimens_20dp"/>
    <solid android:color="@color/dialog_permission_color7" />
</shape>
bash 复制代码
    <string name="dialog_permission_content1">*为保证功能正常,请在系统设置中开启相关权限。</string>
    <string name="dialog_permission_content2">媒体文件访问权限</string>
    <string name="dialog_permission_content3">用于读取和下载您设备上的照片、视频等媒体文件。</string>
    <string name="dialog_permission_content4">定位权限</string>
    <string name="dialog_permission_content5">用于为您提供精确定位,支持车辆位置查看及相关位置服务。</string>
    <string name="dialog_permission_content6">相机权限</string>
    <string name="dialog_permission_content7">用于扫描设备二维码,实现设备快速绑定功能。</string>
    <string name="dialog_permission_content8">通知权限</string>
    <string name="dialog_permission_content9">用于向您发送重要通知,以确保信息的及时传达和同步更新。</string>
    <string name="dialog_permission_content10">麦克风权限</string>
    <string name="dialog_permission_content11">用于获取麦克风音频,支持设备远程直播及语音交互功能。</string>
    <string name="dialog_permission_content12">权限说明</string>
    <string name="dialog_permission_content13">为了保障应用的完整功能体验,应用需要访问相关权限以使用对应的系统功能或资源。</string>
    <string name="dialog_permission_content14">去设置</string>
    <string name="dialog_permission_content15">取消操作</string>
bash 复制代码
    <!-- permission -->
    <color name="dialog_permission_color1">#1D2129</color>
    <color name="dialog_permission_color2">#666666</color>
    <color name="dialog_permission_color3">#333333</color>
    <color name="dialog_permission_color4">#FA5151</color>
    <color name="dialog_permission_color5">#FFFFFF</color>
    <color name="dialog_permission_color6">#86909C</color>
    <color name="dialog_permission_color7">#325EF6</color>
    <color name="dialog_permission_color8">#FFFFFF</color>
PermissionTipDialog

PermissionTipDialog:权限使用告知弹窗,更多请具体参考这篇文章:Android 通用 BaseDialog 实现:支持 ViewBinding + 全屏布局 + 加载弹窗

bash 复制代码
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.NonNull;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.BaseDialog;
import com.xxxx.xxxx.base.MyApplication;
import com.xxxx.xxxx.databinding.DialogPermissionTipBinding;

/**
 * Author: Su
 * Date: 2025/12/20
 * Description: Description
 */

public class PermissionTipDialog extends BaseDialog<DialogPermissionTipBinding> implements View.OnClickListener {

    public PermissionTipDialog(@NonNull Context context) {
        super(context, R.style.BaseDialogStyle);
    }

    @Override
    protected int getLayoutId() {
        return 0;
    }

    @Override
    protected DialogPermissionTipBinding initViewBinding() {
        return DialogPermissionTipBinding.inflate(getLayoutInflater());
    }

    @Override
    protected void initialize() {
        applyFullWidthLayoutWithMargin((int) MyApplication.getFloatDimension(R.dimen.dimens_12dp));
        setGravity(Gravity.TOP);
    }

    @Override
    protected void initListener() {

    }

    @Override
    public void onClick(View v) {

    }

    public void setData(String permissionName, String permissionContent) {
        mViewBinding.tvLayout1.setText(permissionName);
        mViewBinding.tvLayout2.setText(permissionContent);
    }
}
bash 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:background="@drawable/bg_radius8_color1">

    <TextView
        android:id="@+id/tv_layout1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/none"
        android:textSize="@dimen/dimens_16dp"
        android:textColor="@color/dialog_permission_color1"
        android:textStyle="bold"
        android:layout_marginTop="@dimen/dimens_18dp"
        android:layout_marginLeft="@dimen/dimens_10dp"
        android:layout_marginRight="@dimen/dimens_10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <TextView
        android:id="@+id/tv_layout2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/none"
        android:textSize="@dimen/dimens_14dp"
        android:textColor="@color/dialog_permission_color2"
        android:layout_marginTop="@dimen/dimens_6dp"
        android:layout_marginLeft="@dimen/dimens_10dp"
        android:layout_marginRight="@dimen/dimens_10dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"></TextView>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/dimens_18dp"
        app:layout_constraintTop_toBottomOf="@id/tv_layout2"
        app:layout_constraintLeft_toLeftOf="parent"></View>

</androidx.constraintlayout.widget.ConstraintLayout>
bash 复制代码
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="@dimen/dimens_8dp"/>
    <solid android:color="@color/dialog_permission_color8" />
</shape>
bash 复制代码
    <string name="dialog_permission_content1">*为保证功能正常,请在系统设置中开启相关权限。</string>
    <string name="dialog_permission_content2">媒体文件访问权限</string>
    <string name="dialog_permission_content3">用于读取和下载您设备上的照片、视频等媒体文件。</string>
    <string name="dialog_permission_content4">定位权限</string>
    <string name="dialog_permission_content5">用于为您提供精确定位,支持车辆位置查看及相关位置服务。</string>
    <string name="dialog_permission_content6">相机权限</string>
    <string name="dialog_permission_content7">用于扫描设备二维码,实现设备快速绑定功能。</string>
    <string name="dialog_permission_content8">通知权限</string>
    <string name="dialog_permission_content9">用于向您发送重要通知,以确保信息的及时传达和同步更新。</string>
    <string name="dialog_permission_content10">麦克风权限</string>
    <string name="dialog_permission_content11">用于获取麦克风音频,支持设备远程直播及语音交互功能。</string>
    <string name="dialog_permission_content12">权限说明</string>
    <string name="dialog_permission_content13">为了保障应用的完整功能体验,应用需要访问相关权限以使用对应的系统功能或资源。</string>
    <string name="dialog_permission_content14">去设置</string>
    <string name="dialog_permission_content15">取消操作</string>
bash 复制代码
    <!-- permission -->
    <color name="dialog_permission_color1">#1D2129</color>
    <color name="dialog_permission_color2">#666666</color>
    <color name="dialog_permission_color3">#333333</color>
    <color name="dialog_permission_color4">#FA5151</color>
    <color name="dialog_permission_color5">#FFFFFF</color>
    <color name="dialog_permission_color6">#86909C</color>
    <color name="dialog_permission_color7">#325EF6</color>
    <color name="dialog_permission_color8">#FFFFFF</color>
ActivityCollector

ActivityCollector:ActivityCollector 工具类

bash 复制代码
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Author: Su
 * Date: 2025/9/17
 * Description: ActivityCollector 工具类
 *
 * 功能:
 * 1. 使用弱引用维护当前应用的所有 Activity 实例,防止内存泄漏。
 * 2. 提供统一的 Activity 管理方法,包括添加、移除、关闭所有、保留指定 Activity、
 *    关闭指定类型 Activity 等功能。
 * 3. 支持重新登录、跳转清栈等场景,避免使用 FLAG_ACTIVITY_CLEAR_TASK 导致启动图闪烁。
 */

public class ActivityCollector {

    // 维护一个弱引用的 Activity 列表,防止内存泄漏
    // CopyOnWriteArrayList 保证线程安全,遍历和删除不会抛 ConcurrentModificationException
    private static final List<WeakReference<BaseActivity>> activityList = new CopyOnWriteArrayList<>();

    /**
     * 将 Activity 添加到管理列表
     * @param activity 当前创建的 Activity
     *
     * 注意:
     * - Activity 会被弱引用包装,避免因未手动移除而导致内存泄漏。
     */
    public static void addActivity(BaseActivity activity) {
        activityList.add(new WeakReference<>(activity));
    }

    /**
     * 从管理列表中移除 Activity
     * @param activity 当前销毁的 Activity
     *
     * 说明:
     * - 直接移除当前 Activity 或已经被 GC 回收的 Activity。
     * - 使用 removeIf 简化逻辑,同时清理已回收对象。
     */
    public static void removeActivity(BaseActivity activity) {
        activityList.removeIf(ref -> ref.get() == activity || ref.get() == null);
    }

    /**
     * 关闭所有 Activity
     *
     * 场景:
     * - 用户退出登录或应用重置状态时,清理整个 Activity 栈。
     *
     * 注意:
     * - 先判断 !activity.isFinishing() 避免重复调用 finish()。
     * - 清理后调用 activityList.clear() 保证列表为空。
     */
    public static void finishAll() {
        for (WeakReference<BaseActivity> ref : activityList) {
            BaseActivity activity = ref.get();
            if (activity != null && !activity.isFinishing()) {
                activity.finish();
            }
        }
        activityList.clear();
    }

    /**
     * 关闭所有 Activity,但保留指定 Class 类型的 Activity
     * @param keepClass 需要保留的 Activity 类型
     *
     * 场景:
     * - 重新登录场景:启动登录页后,关闭其他页面,但保留登录页实例。
     *
     * 注意:
     * - 通过 Class 判断,避免在 startActivity() 后因为异步创建导致保留错误的实例。
     * - 遍历完后可选择性清理已经被 GC 回收的引用。
     */
    public static void finishAllExcept(Class<?> keepClass) {
        for (WeakReference<BaseActivity> ref : activityList) {
            BaseActivity activity = ref.get();
            // 非目标类并且未 finish 的 Activity 才关闭
            if (activity != null && !activity.getClass().equals(keepClass) && !activity.isFinishing()) {
                activity.finish();
            }
        }
        // 清理已经被回收的 Activity 引用,保持列表干净
        activityList.removeIf(ref -> ref.get() == null);
    }

    /**
     * 关闭指定类型的 Activity,可以传入多个 Class
     * 例如:finishActivities(MainActivity.class, SettingActivity.class)
     * @param activityClasses 需要关闭的 Activity 类型列表
     *
     * 说明:
     * - 支持批量关闭指定页面,而不影响其他 Activity。
     * - 同时自动清理已经被 GC 回收的 Activity 引用。
     * - 遍历列表时使用 removeIf 保证线程安全。
     */
    public static void finishActivities(Class<?>... activityClasses) {
        // 转换成 List,方便 contains 判断
        List<Class<?>> targetList = Arrays.asList(activityClasses);

        // 遍历 activityList 并移除无效引用,同时关闭目标 Activity
        activityList.removeIf(ref -> {
            BaseActivity activity = ref.get();
            if (activity == null) {
                // 弱引用被回收,直接移除
                return true;
            }
            // 如果当前 Activity 类型在目标列表中,则关闭
            if (targetList.contains(activity.getClass())) {
                if (!activity.isFinishing()) {
                    activity.finish();
                }
            }
            // 保留有效引用
            return false;
        });
    }


    // 排除的 Activity 类集合,例如登录页、注册页、忘记密码页等
    private static final Set<Class<?>> EXCLUDED_ACTIVITIES = new HashSet<>();

    static {
//        EXCLUDED_ACTIVITIES.add(UserLoginActivity.class);
//        EXCLUDED_ACTIVITIES.add(UserRegisterActivity.class);
//        EXCLUDED_ACTIVITIES.add(UserForgetActivity.class);
//        EXCLUDED_ACTIVITIES.add(MainActivity.class);
        // 添加其他需要排除的 Activity 类
    }

    /**
     * 判断指定的 Activity 类是否为排除的 Activity。
     *
     * @param activityClass 要判断的 Activity 类
     * @return 如果是排除的 Activity,则返回 true;否则返回 false。
     */
    public static boolean isExcludedActivity(Class<?> activityClass) {
        return EXCLUDED_ACTIVITIES.contains(activityClass);
    }

}
AppInfoUtils

AppInfoUtils:获取当前APP的信息

bash 复制代码
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import androidx.core.content.FileProvider;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.MyApplication;
import java.io.File;

/**
 * Author: Su
 * Date: 2025/4/3
 * Description: 获取当前APP的信息
 */

public class AppInfoUtils {

    public static final String TENCENT_PACKAGE = "com.tencent.mm";
    public static final String GAO_DE_PACKAGE = "com.autonavi.minimap";

    /**
     * 获取应用包名
     */
    public static String getPackageName(Context context) {
        return context.getPackageName();
    }

    /**
     * 获取应用版本名称(Version Name)
     */
    public static String getVersionName(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return "1.0";
        }
    }

    /**
     * 获取应用版本号(Version Code)
     */
    public static int getVersionCode(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return -1;
        }
    }

    /**
     * 获取应用名称(App Name)
     */
    public static String getAppName(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            ApplicationInfo applicationInfo = pm.getApplicationInfo(context.getPackageName(), 0);
            return pm.getApplicationLabel(applicationInfo).toString();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return "Unknown";
        }
    }

    /**
     * 获取应用图标(App Icon)
     */
    public static Drawable getAppIcon(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            ApplicationInfo applicationInfo = pm.getApplicationInfo(context.getPackageName(), 0);
            return pm.getApplicationIcon(applicationInfo);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 判断指定包名的应用是否已安装
     *
     * @param context 应用上下文
     * @param packageName 应用包名(如 "com.tencent.mm")
     * @return 已安装返回 true,否则返回 false
     */
    public static boolean isAppInstalled(Context context, String packageName) {
        if (context == null || packageName == null || packageName.isEmpty()) {
            return false;
        }
        try {
            context.getPackageManager().getPackageInfo(packageName, 0);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }


    /**
     * 安装指定路径的 APK 文件
     * @param context 上下文
     * @param path APK 文件路径(如 /storage/emulated/0/Download/update.apk)
     */
    public static void installApkByPath(Context context, String path) {
        File apkFile = new File(path);
        if (!apkFile.exists()) {
            ToastUtils.showCenterNoIcon(MyApplication.getStringResource(R.string.dialog_app_upload_content8));
            return;
        }

        // 构造安装意图
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri apkUri;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            // Android 7.0 及以上使用 FileProvider 获取 content 类型 Uri
            apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else {
            // Android 7.0 以下可以使用 file:// 直接访问
            apkUri = Uri.fromFile(apkFile);
        }

        // 设置 APK 文件的 MIME 类型
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");

        context.startActivity(intent);
    }

    // 跳转应用详情页,让用户手动开启权限
    public static void openAppSettings(Context context) {
        Intent intent = new Intent();
        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", context.getPackageName(), null);
        intent.setData(uri);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }

}

AuthManager:账号信息 存储等

bash 复制代码
public class AuthManager {

    // ===================== 隐私政策 =====================

    public static void setPrivacyPolicyAccepted(boolean isPrivacyPolicyAccepted) {
        SPUtils.getInstance().put(SPKeyTag.PrivacyPolicyAcceptedTag, isPrivacyPolicyAccepted);
    }

    public static boolean isPrivacyPolicyAccepted() {
        return SPUtils.getInstance().get(SPKeyTag.PrivacyPolicyAcceptedTag, true);
    }

    // ===================== 账号、密码、工位号 =====================

    public static void saveLoginAccount(String account, String password, String workStationNumber) {
        // 可选:添加加密处理
        SPUtils.getInstance().put(SPKeyTag.LoginAccountTag, account);
        SPUtils.getInstance().put(SPKeyTag.LoginPasswordTag, password);
        SPUtils.getInstance().put(SPKeyTag.WorkStationNumberTag, workStationNumber);
    }

    public static String getLoginAccount() {
        return SPUtils.getInstance().get(SPKeyTag.LoginAccountTag, "");
    }

    public static String getLoginPassword() {
        return SPUtils.getInstance().get(SPKeyTag.LoginPasswordTag, "");
    }
    public static String getWorkStationNumber() {
        return SPUtils.getInstance().get(SPKeyTag.WorkStationNumberTag, "");
    }

    // ===================== 用户 Token =====================

    public static void saveUserToken(String userToken) {
        SPUtils.getInstance().put(SPKeyTag.AuthLoginTokenTag, userToken);
    }

    public static String getUserToken() {
        return SPUtils.getInstance().get(SPKeyTag.AuthLoginTokenTag, "");
    }

    // ===================== 清除用户信息相关 =====================

    public static void clearUserInfo() {
        SPUtils.getInstance().remove(SPKeyTag.LoginAccountTag);
        SPUtils.getInstance().remove(SPKeyTag.LoginPasswordTag);
        SPUtils.getInstance().remove(SPKeyTag.WorkStationNumberTag);
        SPUtils.getInstance().remove(SPKeyTag.AuthLoginTokenTag);
    }

    // ===================== 环境切换 =====================
    private static boolean devTestSwitch = true;

    public static boolean isDevTestSwitch() {
        return devTestSwitch;
    }

    public static void setDevTestSwitch(boolean devTestSwitch) {
        AuthManager.devTestSwitch = devTestSwitch;
    }
}
EnumUtils

EnumUtils:泛型

bash 复制代码
public class EnumUtils {

    //扫码类型
    public enum ScannerType {
        UNKNOW,
        SCANNER_TYPE_NUMBER,    // 工单号
        SCANNER_TYPE_SN       // SN
    }

    //网络策略枚举
    public enum NetworkPolicy {
        DEFAULT,        // 系统默认网络(4G / Wi-Fi)
        WIFI_ONLY       // 指定 Wi-Fi Network
    }
    
    // 定义点击事件的枚举类型
    public enum EventListenerAction {
        SAVE,    // 保存操作
        CONFIRM,    // 确认操作
        CANCEL,     // 取消操作
        DELETE,     // 删除操作
        LIKE,       // 点赞操作
        SELECT,     // 选择操作
        UPDATE,     // 更新操作
        COPY,     // 复制操作
        COMPLAINT,     // 投诉操作
        BLOCK,     // 屏蔽
        ITEM_CLICK, // 列表项点击操作
        ITEM_LONG_CLICK, // 列表项长按操作
        LOADING,    // 列表加载操作
        FIR_COMMENTS_CLICK,    // 点击一级评论
        FIR_COMMENTS_LONG_CLICK,    // 长按一级评论
        SEC_COMMENTS_CLICK,    // 点击二级评论
        DESTROY,       // 销毁
        MQTT_CONNECT_SUCCESS,       // Mqtt连接成功
        MQTT_CONNECT_FAIL,       // Mqtt连接失败
        MQTT_CONNECT_LOST,       // Mqtt连接丢失
        MQTT_PUBLISH_SUCCESS,       // Mqtt发送成功
        MQTT_PUBLISH_FAIL,       // Mqtt发送失败
        MQTT_SUBSCRIBE_SUCCESS,       // Mqtt订阅成功subscribe
        MQTT_SUBSCRIBE_FAIL,       // Mqtt订阅失败
        MQTT_SUBSCRIBE_FAIL_ALREADY,       // Mqtt已订阅
        MQTT_PUBLISH_FAIL_NO_CONNECT,       // Mqtt未连接,无法发送
        MQTT_UNSUBSCRIBE_FAIL_NO_SUBSCRIBE,       // Mqtt未订阅
        MQTT_UNSUBSCRIBE_SUCCESS,       // Mqtt取消订阅成功subscribe
        MQTT_UNSUBSCRIBE_FAIL,       // Mqtt取消订阅失败
        SHOW,       // 日期选择器 show
        CHANGE,       // 日期选择器 日期change
        HEADER,       // 上传头像
        SEND_CODE,       // 发送验证码
        USER_AGREEMENT,       // 查看用户协议
        PRIVACY_POLICY,       // 查看隐私政策
        ADD_IMAGE,       // 添加图片
        CHECK_IMAGE,       // 查看图片
        DELETE_IMAGE,       // 删除图片
        WX_SHARE,       // 微信分享
        WX_MOMENTS_SHARE,       // 朋友圈分享
        INSTALL,       // 安装
        REFRESH,       // 刷新
        SUCCESS,    //成功
        FAILURE,    //失败
        CALL_PHONE,    //拨打电话
        HANDLE_EVENT,    //处理事件
        SUCCESS_PASS,    //测试通过
        FAILED_PASS    //测试不通过
    }
}
EventBusUtils

EventBusUtils:EventBus 工具类:封装注册、注销、发送等操作,防止重复调用。

bash 复制代码
import org.greenrobot.eventbus.EventBus;

/**
 * Author: Su
 * Date: 2025/5/12
 * Description: EventBus 工具类:封装注册、注销、发送等操作,防止重复调用。
 */
public class EventBusUtils {

    /**
     * 注册事件,防止重复注册
     * @param subscriber 订阅者对象(如 Activity、Fragment)
     */
    public static void register(Object subscriber) {
        if (!EventBus.getDefault().isRegistered(subscriber)) {
            EventBus.getDefault().register(subscriber);
        }
    }

    /**
     * 取消注册事件,防止未注册对象被反复注销引发异常
     * @param subscriber 订阅者对象(如 Activity、Fragment)
     */
    public static void unregister(Object subscriber) {
        if (EventBus.getDefault().isRegistered(subscriber)) {
            EventBus.getDefault().unregister(subscriber);
        }
    }

    /**
     * 发送普通事件(主线程或子线程)
     * @param tag,object 指定事件对象
     */
    public static void post(String tag, Object object) {
        EventBusBean eventBusBean = new EventBusBean(object, tag);
        EventBus.getDefault().post(eventBusBean);
    }

    /**
     * 发送普通事件(主线程或子线程)
     * @param event 任意事件对象
     */
    public static void post(Object event) {
        EventBus.getDefault().post(event);
    }

    /**
     * 发送粘性事件(用于先发后收场景,如初始化完成后再注册监听)
     * @param event 任意事件对象
     */
    public static void postSticky(Object event) {
        EventBus.getDefault().postSticky(event);
    }

    /**
     * 移除指定类型的粘性事件
     * @param eventType 粘性事件 class 类型
     */
    public static void removeStickyEvent(Class<?> eventType) {
        Object event = EventBus.getDefault().getStickyEvent(eventType);
        if (event != null) {
            EventBus.getDefault().removeStickyEvent(event);
        }
    }

    /**
     * 清除所有粘性事件
     */
    public static void removeAllStickyEvents() {
        EventBus.getDefault().removeAllStickyEvents();
    }

    /**
     * 检查对象是否已注册
     * @param subscriber 要检查的对象
     * @return 是否已注册
     */
    public static boolean isRegistered(Object subscriber) {
        return EventBus.getDefault().isRegistered(subscriber);
    }
}
EventListener

EventListener:通用事件监听器接口,适用于各种用户交互事件的回调处理。

bash 复制代码
/**
 * Author: Su
 * Date: 2025/12/20
 * Description:
 * 通用事件监听器接口,适用于各种用户交互事件的回调处理。
 *
 * @param <T> 与事件相关的数据类型,根据具体需求进行指定。
 */
public abstract class EventListener<T> {

    /**
     * 当用户执行某个操作时调用。
     *
     * @param action 表示用户执行的操作类型。
     */
    public void onAction(EnumUtils.EventListenerAction action) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data) {}


    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param data1   与操作相关的数据对象。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, T data1) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param data1   与操作相关的数据对象。
     * @param data2   与操作相关的数据对象。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, T data1, T data2) {}
    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param data1   与操作相关的数据对象。
     * @param data2   与操作相关的数据对象。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, T data1, T data2, T data3) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param position 与操作相关的位置索引。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, int position) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param parentPosition 与操作相关的位置索引。
     * @param childPosition 与操作相关的位置索引。
     */
    public void onAdapterAction(EnumUtils.EventListenerAction action, T data, int parentPosition, int childPosition) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象。
     * @param position 与操作相关的位置索引。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, Long position) {}

    /**
     * 当用户执行某个操作,并传递了相关数据时调用。
     *
     * @param action 表示用户执行的操作类型。
     * @param data   与操作相关的数据对象(如评论)。
     * @param position 与操作相关的数据在其所属列表中的位置索引。
     * @param parentData 如果是二级评论,表示其所属的一级评论数据;如果是一级评论,则为 null。
     * @param parentPosition 如果是二级评论,表示其所属的一级评论在列表中的位置索引;如果是一级评论,则为 -1。
     */
    public void onAction(EnumUtils.EventListenerAction action, T data, int position, T parentData, int parentPosition) {
        // 实现逻辑
    }

    /**
     * 当操作被取消时调用。
     */
    public void onCancel() {}

}
PermissionUtils

PermissionUtils:动态权限工具类

bash 复制代码
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.MyApplication;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 动态权限工具类:简化Android运行时权限申请,支持多组请求独立回调,包含悬浮窗权限检测
 * <p>
 * Author: Su
 * Date: 2025/05/06
 * <p>
 * 功能特性:
 * 1. 自动生成唯一请求码,支持同时发起多组权限请求
 * 2. 提供权限检查、申请、结果回调一站式处理
 * 3. 支持悬浮窗权限检测及跳转设置
 * 4. 使用弱引用持有回调,避免内存泄漏
 * 5. 支持Activity/Fragment权限申请
 */
public class PermissionUtils {

    /**
     * 权限请求结果回调接口
     */
    public interface PermissionCallback {
        /**
         * 所有权限均已授予
         */
        void onGranted();

        /**
         * 部分或全部权限被拒绝
         * @param deniedPermissions 被拒绝的权限列表
         * @param permanentDenied   是否包含被永久拒绝的权限(用户勾选不再询问)
         */
        void onDenied(List<String> deniedPermissions, boolean permanentDenied);
    }

    // 请求码生成器(线程安全)
    private static final AtomicInteger sRequestCodeGenerator = new AtomicInteger(1000);

    // 使用WeakReference避免Activity/Fragment内存泄漏
    private static final SparseArray<WeakReference<PermissionCallback>> sCallbackMap = new SparseArray<>();

    /**
     * 检查单个权限是否已授予
     * @param context    上下文对象
     * @param permission 要检查的权限
     * @return true-已授权,false-未授权
     */
    public static boolean isPermissionGranted(@NonNull Context context, @NonNull String permission) {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * 请求权限(Activity版本)
     * @param activity   目标Activity
     * @param permissions 需要请求的权限数组
     * @param callback   结果回调
     * @return 本次请求的requestCode(直接授予时为-1)
     */
    public static int requestPermissions(@NonNull Activity activity,
                                         @NonNull String[] permissions,
                                         @NonNull PermissionCallback callback) {
        return requestPermissionsImpl(activity, permissions, callback);
    }

    /**
     * 请求权限(Fragment版本)
     * @param fragment   目标Fragment
     * @param permissions 需要请求的权限数组
     * @param callback   结果回调
     * @return 本次请求的requestCode(直接授予时为-1)
     */
    public static int requestPermissions(@NonNull Fragment fragment,
                                         @NonNull String[] permissions,
                                         @NonNull PermissionCallback callback) {
        return requestPermissionsImpl(fragment, permissions, callback);
    }

    // 统一的权限请求实现
    private static <T> int requestPermissionsImpl(@NonNull T host,
                                                  @NonNull String[] permissions,
                                                  @NonNull PermissionCallback callback) {
        Context context = getContext(host);
        if (context == null) return -1;

        List<String> deniedList = new ArrayList<>();
        for (String perm : permissions) {
            if (!isPermissionGranted(context, perm)) {
                deniedList.add(perm);
            }
        }

        if (deniedList.isEmpty()) {
            callback.onGranted();
            return -1;
        }

        int requestCode = sRequestCodeGenerator.getAndIncrement();
        sCallbackMap.put(requestCode, new WeakReference<>(callback));

        if (host instanceof Activity) {
            ActivityCompat.requestPermissions((Activity) host,
                    deniedList.toArray(new String[0]), requestCode);
        } else if (host instanceof Fragment) {
            ((Fragment) host).requestPermissions(
                    deniedList.toArray(new String[0]), requestCode);
        }

        return requestCode;
    }

    /**
     * 必须在Activity/Fragment的onRequestPermissionsResult中调用
     * @param requestCode  请求码
     * @param permissions  权限数组
     * @param grantResults 授权结果
     */
    public static void onRequestPermissionsResult(int requestCode,
                                                  @NonNull String[] permissions,
                                                  @NonNull int[] grantResults) {
        WeakReference<PermissionCallback> ref = sCallbackMap.get(requestCode);
        if (ref == null) return;

        PermissionCallback callback = ref.get();
        sCallbackMap.remove(requestCode);

        if (callback == null) return;

        Context context = getCurrentContext(); // 需要实现获取当前Context的方法(例如通过Application)
        boolean hasPermanentDenied = false;
        List<String> deniedList = new ArrayList<>();

        for (int i = 0; i < grantResults.length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                String perm = permissions[i];
                deniedList.add(perm);

                // 判断是否永久拒绝
                if (context != null &&
                        !ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, perm)) {
                    hasPermanentDenied = true;
                }
            }
        }

        if (deniedList.isEmpty()) {
            callback.onGranted();
        } else {
            callback.onDenied(deniedList, hasPermanentDenied);
        }
    }

    /**
     * 检查悬浮窗权限是否授予
     */
    public static boolean hasOverlayPermission(@NonNull Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true;
        return Settings.canDrawOverlays(context);
    }

    /**
     * 跳转到悬浮窗权限设置页面
     * @param activity    目标Activity
     * @param requestCode 请求码(用于onActivityResult)
     */
    public static void requestOverlayPermission(@NonNull Activity activity, int requestCode) {
        if (hasOverlayPermission(activity)) return;

        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:" + activity.getPackageName()));
        activity.startActivityForResult(intent, requestCode);
    }

    // 获取上下文对象
    private static Context getContext(Object host) {
        if (host instanceof Activity) {
            return (Activity) host;
        } else if (host instanceof Fragment) {
            return ((Fragment) host).getContext();
        }
        return null;
    }

    // 获取当前上下文(需要根据实际情况实现)
    private static Context getCurrentContext() {
        // 可以通过Application或其他方式获取,此处需要具体实现
        return null;
    }

    /**
     * 检查一组权限是否全部授予
     *
     * @param context     上下文对象,建议传入 Activity 或 Application
     * @param permissions 要检查的权限数组
     * @return true - 全部权限已授权;false - 至少存在一个未授权
     */
    public static boolean isPermissionGranted(@NonNull Context context, @NonNull String... permissions) {
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    /**
     * 根据权限列表返回统一的权限名称和描述(适用于多个权限对应同一用途)
     *
     * @param permissions 权限列表
     * @return Pair:first = 权限名称,second = 权限用途说明
     */
    public static Pair<String, String> getUnifiedPermissionDescription(List<String> permissions) {
        for (String permission : permissions) {
            switch (permission) {

                case Manifest.permission.READ_EXTERNAL_STORAGE:
                case Manifest.permission.WRITE_EXTERNAL_STORAGE:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content2),
                            MyApplication.getStringResource(R.string.dialog_permission_content3)
                    );

                case Manifest.permission.READ_MEDIA_IMAGES:
                case Manifest.permission.READ_MEDIA_VIDEO:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content2),
                            MyApplication.getStringResource(R.string.dialog_permission_content3)
                    );

                case Manifest.permission.ACCESS_COARSE_LOCATION:
                case Manifest.permission.ACCESS_FINE_LOCATION:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content4),
                            MyApplication.getStringResource(R.string.dialog_permission_content5)
                    );

                case Manifest.permission.CAMERA:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content6),
                            MyApplication.getStringResource(R.string.dialog_permission_content7)
                    );

                case Manifest.permission.POST_NOTIFICATIONS:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content8),
                            MyApplication.getStringResource(R.string.dialog_permission_content9)
                    );

                case Manifest.permission.RECORD_AUDIO:
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content10),
                            MyApplication.getStringResource(R.string.dialog_permission_content11)
                    );

                default:
                    // 如果是未知权限
                    return new Pair<>(
                            MyApplication.getStringResource(R.string.dialog_permission_content12),
                            MyApplication.getStringResource(R.string.dialog_permission_content13)
                    );
            }
        }
        return new Pair<>(
                MyApplication.getStringResource(R.string.dialog_permission_content12),
                MyApplication.getStringResource(R.string.dialog_permission_content13)
        );
    }

}
bash 复制代码
    <!-- permission -->
    <string name="dialog_permission_content1">*为保证功能正常,请在系统设置中开启相关权限。</string>
    <string name="dialog_permission_content2">媒体文件访问权限</string>
    <string name="dialog_permission_content3">用于读取和下载您设备上的照片、视频等媒体文件。</string>
    <string name="dialog_permission_content4">定位权限</string>
    <string name="dialog_permission_content5">用于为您提供精确定位,支持车辆位置查看及相关位置服务。</string>
    <string name="dialog_permission_content6">相机权限</string>
    <string name="dialog_permission_content7">用于扫描设备二维码,实现设备快速绑定功能。</string>
    <string name="dialog_permission_content8">通知权限</string>
    <string name="dialog_permission_content9">用于向您发送重要通知,以确保信息的及时传达和同步更新。</string>
    <string name="dialog_permission_content10">麦克风权限</string>
    <string name="dialog_permission_content11">用于获取麦克风音频,支持设备远程直播及语音交互功能。</string>
    <string name="dialog_permission_content12">权限说明</string>
    <string name="dialog_permission_content13">为了保障应用的完整功能体验,应用需要访问相关权限以使用对应的系统功能或资源。</string>
    <string name="dialog_permission_content14">去设置</string>
    <string name="dialog_permission_content15">取消操作</string>
StringUtils

StringUtils:字符串工具类,封装了常用字符串操作方法,包含防空处理、拼接、拆分等

bash 复制代码
import com.xxxx.xxxx.R;
import com.xxxx.xxxx.base.MyApplication;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * Author: Su
 * Date: 2025/4/7
 * Description: 字符串工具类,封装了常用字符串操作方法,包含防空处理、拼接、拆分等
 */
public class StringUtils {

    /**
     * 判断字符串是否为空,如果为空则返回占位符,否则返回原字符串。
     *
     * @param string      待判断的字符串
     * @param placeholder 当字符串为空或 null 时返回的占位内容
     * @return 非空字符串原样返回;否则返回占位符
     */
    public static String stringEmpty(String string, String placeholder) {
        if (!isBlank(string)) {
            return string;
        }
        return placeholder;
    }

    /**
     * 判断字符串是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param string      待判断的字符串
     * @param placeholder 资源 id,当字符串为空时取此资源内容
     * @return 非空字符串原样返回;否则返回资源内容
     */
    public static String stringEmpty(String string, int placeholder) {
        if (!isBlank(string)) {
            return string;
        }
        return MyApplication.getStringResource(placeholder);
    }

    /**
     * 判断字符串是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param string      待判断的字符串
     * @param placeholder 资源 id,当字符串为空时取此资源内容
     * @return 非空字符串原样返回;否则返回资源内容
     */
    public static String stringEmpty(Integer string, int placeholder) {
        if (!isBlank(string)) {
            return string.toString();
        }
        return MyApplication.getStringResource(placeholder);
    }

    /**
     * 判断字符串是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param string      待判断的字符串
     * @param placeholder 资源 id,当字符串为空时取此资源内容
     * @return 非空字符串原样返回;否则返回资源内容
     */
    public static String stringEmpty(Double string, String placeholder) {
        if (!isBlank(string)) {
            return string.toString();
        }
        return placeholder;
    }


    /**
     * 判断字符串是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param string      待判断的字符串
     * @param placeholder 资源 id,当字符串为空时取此资源内容
     * @return 非空字符串原样返回;否则返回资源内容
     */
    public static String stringEmpty(Integer string, String placeholder) {
        if (!isBlank(string)) {
            return string.toString();
        }
        return placeholder;
    }

    /**
     * 判断字符串是否为 null 或全是空白字符
     *
     * @param string 字符串
     * @return true 表示字符串为 null 或空字符串或只包含空白字符
     */
    public static boolean isBlank(String string) {
        return string == null || string.trim().isEmpty();
    }

    /**
     * 判断字符串是否为 null 或全是空白字符
     *
     * @param string 字符串
     * @return true 表示字符串为 null 或空字符串或只包含空白字符
     */
    public static boolean isBlank(Integer string) {
        return string == null;
    }

    /**
     * 判断字符串是否为 null 或全是空白字符
     *
     * @param string 字符串
     * @return true 表示字符串为 null 或空字符串或只包含空白字符
     */
    public static boolean isBlank(Double string) {
        return string == null;
    }

    /**
     * 判断 Integer 是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param number       待判断的 Integer
     * @param placeholder  资源 id,当 number 为空时取此资源内容
     * @return 非空则返回 number.toString();否则返回资源内容
     */
    public static String intEmpty(Integer number, int placeholder) {
        if (number != null) {
            return number.toString();
        }
        return MyApplication.getStringResource(placeholder);
    }


    /**
     * 判断 Integer 是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param number       待判断的 Integer
     * @param placeholder  资源 id,当 number 为空时取此资源内容
     * @return 非空则返回 number.toString();否则返回资源内容
     */
    public static int intNumEmpty(Integer number, int placeholder) {
        if (number != null) {
            return number;
        }
        return placeholder;
    }

    /**
     * 判断 Integer 是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param number       待判断的 Integer
     * @param placeholder  当 number 为空时取此资源内容
     * @return 非空则返回 number.toString();否则返回资源内容
     */
    public static String intEmpty(Integer number, String placeholder) {
        if (number != null) {
            return number.toString();
        }
        return placeholder;
    }
    /**
     * 判断 Double 是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param number       待判断的 Integer
     * @param placeholder  当 number 为空时取此资源内容
     * @return 非空则返回 number.toString();否则返回资源内容
     */
    public static double doubleEmpty(Double number, double placeholder) {
        return number != null ? number : placeholder;
    }

    /**
     * 判断 Integer 是否为空,如果为空则返回指定资源中的占位字符串。
     *
     * @param number       待判断的 Integer
     * @param placeholder  当 number 为空时取此资源内容
     * @return 非空则返回 number.toString();否则返回资源内容
     */
    public static int intEmptyPlaceholder(Integer number, int placeholder) {
        if (number != null) {
            return number;
        }
        return placeholder;
    }


    /**
     * 用指定的分隔符分割字符串为数组
     *
     * @param source 待分割字符串
     * @param delimiter 分隔符(支持正则)
     * @return 分割后的字符串数组,若 source 为空返回空数组
     */
    public static String[] split(String source, String delimiter) {
        if (isBlank(source)) {
            return new String[0];
        }
        return source.split(delimiter);
    }

    /**
     * 使用指定连接符将字符串数组拼接成一个字符串
     *
     * @param elements 字符串数组
     * @param delimiter 分隔符
     * @return 拼接后的字符串
     */
    public static String join(String[] elements, String delimiter) {
        if (elements == null || elements.length == 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < elements.length; i++) {
            sb.append(elements[i]);
            if (i != elements.length - 1) {
                sb.append(delimiter);
            }
        }
        return sb.toString();
    }

    /**
     * 判断两个字符串是否相等,忽略大小写和空值
     *
     * @param a 字符串 a
     * @param b 字符串 b
     * @return 是否相等
     */
    public static boolean equalsIgnoreNull(String a, String b) {
        if (a == null && b == null) return true;
        if (a == null || b == null) return false;
        return a.equalsIgnoreCase(b);
    }

    /**
     * 安全地截取字符串
     *
     * @param source 原字符串
     * @param maxLength 最大长度
     * @return 截取后的字符串,如果原始长度小于 maxLength 则返回原字符串
     */
    public static String safeSubstring(String source, int maxLength) {
        if (isBlank(source)) return "";
        return source.length() <= maxLength ? source : source.substring(0, maxLength);
    }

    /**
     * 清除字符串中的所有空格、换行、制表符等空白字符
     *
     * @param input 输入字符串
     * @return 清理后的字符串
     */
    public static String removeAllWhitespaces(String input) {
        return isBlank(input) ? "" : input.replaceAll("\\s+", "");
    }

    /**
     * 将首字母转为大写
     *
     * @param input 字符串
     * @return 首字母大写字符串
     */
    public static String capitalize(String input) {
        if (isBlank(input)) return "";
        return input.substring(0, 1).toUpperCase() + input.substring(1);
    }

    /**
     * 将首字母转为小写
     *
     * @param input 字符串
     * @return 首字母小写字符串
     */
    public static String decapitalize(String input) {
        if (isBlank(input)) return "";
        return input.substring(0, 1).toLowerCase() + input.substring(1);
    }

    /**
     * 校验 Wi-Fi 名称是否合法
     * 规则:仅允许字母(大小写)、数字、下划线、短横线组成
     *
     * @param val 要校验的 Wi-Fi 名称
     * @return true 表示合法,false 表示不合法
     */
    public static boolean verifyWifiName(String val) {
        // 正则表达式:以 ^ 开始,$ 结束;中间只能包含字母、数字、下划线 _、短横线 -
        String regExp = "^[a-z0-9A-Z_\\-]+$";
        // 判断字符串是否匹配该正则表达式
        boolean flag = val.matches(regExp);
        return flag;
    }

    /**
     * 校验 Wi-Fi 密码是否合法
     * 规则:仅允许字母(大小写)和数字,不允许符号
     *
     * @param val 要校验的 Wi-Fi 密码
     * @return true 表示合法,false 表示不合法
     */
    public static boolean verifyWifiPsw(String val) {
        // 正则表达式:只允许大小写字母和数字
        String regExp = "^[a-z0-9A-Z]+$";
        boolean flag = val.matches(regExp);
        return flag;
    }

    /**
     * 获取字符串中最后一位字符,如果是 9 则替换 0
     *
     * @param input 输入的字符串(一般为数字字符串,例如手机号、编号等)
     * @return 最后一位字符,如果为空或 null 则返回空字符 '\0'
     */
    public static char getLastChar(String input) {
        if (input != null && input.length() > 0) {
            // 返回字符串最后一位字符
            return input.charAt(input.length() - 1);
        }
        // 如果输入为空或长度为 0,返回空字符
        return '\0';
    }

    /**
     * 获取输入字符串最后一位字符(数字),
     * 如果最后一位是 '9' 则返回 0,否则返回该数字本身。
     * <p>
     * 示例:
     *  - 输入 "8666656519" → 返回 0
     *  - 输入 "12345678"   → 返回 8
     * </p>
     *
     * @param input 输入的字符串(应包含数字)
     * @return 最后一位数字(如果为 9 则返回 0)
     * @throws IllegalArgumentException 如果输入为空或最后一位不是数字
     */
    public static int getLastDigitConverted(String input) {
        // 1. 判断输入是否为空
        if (input == null || input.isEmpty()) {
            return 0;
        }

        // 2. 获取最后一个字符
        char lastChar = input.charAt(input.length() - 1);

        // 3. 判断最后一个字符是否为数字
        if (!Character.isDigit(lastChar)) {
           return 0;
        }

        // 4. 转换成数字
        int lastDigit = Character.getNumericValue(lastChar);

        // 5. 如果最后一位为 9,返回 0;否则返回该数字
        return lastDigit == 9 ? 0 : lastDigit;
    }

    /**
     * 将数格式化为两位数字符串,例如:
     * 1 → "01",9 → "09",10 → "10"
     *
     * @param num (整数)
     * @return 格式化后的两位字符串
     */
    public static String formatNumber(int num) {
        return String.format("%02d", num);
    }

    /**
     * 将数字字符串格式化,去除多余的 0,并限制为最多 2 位小数。
     * <p>
     * 如果传入为空或不是有效数字,则返回默认值。
     *
     * @param numberStr   原始数字字符串
     * @param defaultStr  默认值,当 numberStr 为空或非法时返回
     * @return 格式化后的数字字符串或默认值
     *
     * 功能说明:
     * 1. **空值或非法数字处理**:如果输入为空或不是数字,直接返回 defaultStr。
     * 2. **去掉末尾 0**:如 "85.5000" -> "85.5"。
     * 3. **最多 2 位小数**:小数位超过 2 位时,四舍五入保留 2 位小数。
     * 4. **避免科学计数法**:防止显示为 "1E+6",输出普通数字。
     *
     * 示例:
     * - 输入:"85.5000",默认值:"0"  -> 输出:"85.5"
     * - 输入:"85.5678",默认值:"0"  -> 输出:"85.57"
     * - 输入:"",默认值:"0"          -> 输出:"0"
     * - 输入:"abc",默认值:"0"       -> 输出:"0"
     */
    public static String formatNumberWithDefault(String numberStr, String defaultStr) {
        // 1. 如果输入为空或全是空格,直接返回默认值
        if (isBlank(numberStr)) {
            return defaultStr;
        }

        try {
            // 2. 使用 BigDecimal 处理,避免浮点数精度问题
            BigDecimal bd = new BigDecimal(numberStr);

            // 3. 去掉末尾无用的 0,例如 "85.5000" -> "85.5"
            bd = bd.stripTrailingZeros();

            // 4. 限制最多 2 位小数,超出部分四舍五入
            if (bd.scale() > 2) {
                bd = bd.setScale(2, RoundingMode.HALF_UP);
            }

            // 5. 转为普通字符串,避免科学计数法
            return bd.toPlainString();

        } catch (NumberFormatException e) {
            // 6. 输入不是合法数字,返回默认值
            return defaultStr;
        }
    }

    /**
     * 格式化数字字符串:
     * - 如果是整数,保持整数(如 "85" -> "85")
     * - 如果是 1 位小数,保持 1 位(如 "85.5" -> "85.5")
     * - 如果是 2 位小数,保持 2 位(如 "85.56" -> "85.56")
     * - 如果超过 2 位小数,则四舍五入保留 2 位(如 "85.567" -> "85.57")
     * - 自动去掉小数点后多余的 0(如 "85.00" -> "85")
     *
     * @param numberStr 输入的数字字符串
     * @return 格式化后的数字字符串
     */
    public static String formatNumber(String numberStr) {
        try {
            // 使用 BigDecimal 处理,可以避免浮点数精度问题
            BigDecimal bd = new BigDecimal(numberStr);

            // 去掉末尾多余的 0(比如 "85.5000" -> "85.5")
            bd = bd.stripTrailingZeros();

            // 判断当前小数位数(scale 表示小数点后位数)
            if (bd.scale() > 2) {
                // 如果超过 2 位小数,四舍五入保留 2 位
                bd = bd.setScale(2, RoundingMode.HALF_UP);
            }

            // 使用 toPlainString(),避免 BigDecimal 默认转成科学计数法(如 1E+6)
            return bd.toPlainString();
        } catch (NumberFormatException e) {
            // 如果输入不是数字字符串,直接原样返回,避免崩溃
            return numberStr;
        }
    }

    /**
     * 格式化数字:
     * - 如果是整数,保持整数(如 85 -> "85")
     * - 如果是 1 位小数,保持 1 位(如 85.5 -> "85.5")
     * - 如果是 2 位小数,保持 2 位(如 85.56 -> "85.56")
     * - 如果超过 2 位小数,则四舍五入保留 2 位(如 85.567 -> "85.57")
     * - 自动去掉小数点后多余的 0(如 85.00 -> "85")
     *
     * @param number 输入的 double 数字
     * @return 格式化后的数字字符串
     */
    public static String formatNumber(double number) {
        // 使用 BigDecimal 来处理,避免 double 精度丢失
        BigDecimal bd = BigDecimal.valueOf(number);

        // 去掉末尾多余的 0(比如 85.5000 -> 85.5)
        bd = bd.stripTrailingZeros();

        // 判断当前小数位数(scale 表示小数点后位数)
        if (bd.scale() > 2) {
            // 如果超过 2 位小数,四舍五入保留 2 位
            bd = bd.setScale(2, RoundingMode.HALF_UP);
        }

        // 使用 toPlainString(),避免科学计数法(如 1E+6)
        return bd.toPlainString();
    }
}
相关推荐
消失的旧时光-19433 小时前
Android 接入 Flutter(Add-to-App)最小闭环:10 分钟跑起第一个混合页面
android·flutter
城东米粉儿3 小时前
android StrictMode 笔记
android
Zender Han3 小时前
Flutter Android 启动页 & App 图标替换(不使用任何插件的完整实践)
android·flutter·ios
童无极3 小时前
Android 弹幕君APP开发实战01
android
赛恩斯4 小时前
kotlin 为什么可以在没有kotlin 环境的安卓系统上运行的
android·开发语言·kotlin
于山巅相见4 小时前
【3588】Android动态隐藏导航栏
android·导航栏·状态栏·android11
乡野码圣4 小时前
【RK3588 Android12】开发效率提升技巧
android·嵌入式硬件
eybk4 小时前
Beeware生成安卓apk取得系统tts语音朗读例子
android