在大型 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();
}
}