源码整理

This commit is contained in:
2025-12-23 13:12:17 +08:00
parent c2def0bb3b
commit 4e84ff493b
22 changed files with 2365 additions and 1656 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Dec 22 15:19:14 GMT 2025
#Tue Dec 23 04:54:11 GMT 2025
stageCount=24
libraryProject=
baseVersion=15.14
publishVersion=15.14.23
buildCount=3
buildCount=4
baseBetaVersion=15.14.24

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.powerbell.activities;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/22 13:21
* @Describe BatteryReportActivity
*/
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -36,111 +31,87 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 电池报告页面统计应用24小时运行时长与电池消耗情况
* 支持应用搜索、累计耗电计算、电池广播监听,适配 API30
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/10/22 13:21
*/
public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "BatteryReportActivity";
private static final long ONE_DAY_MS = 24 * 3600 * 1000; // 24小时毫秒数
private static final long ONE_MINUTE_MS = 60 * 1000; // 1分钟毫秒数
private Toolbar mToolbar;
// ======================== 成员变量 =========================
// UI组件
private Toolbar mToolbar;
private RecyclerView rvBatteryReport;
private EditText etSearch;
// 数据与适配器
private BatteryReportAdapter adapter;
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();
private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();
// 电池相关
private BroadcastReceiver batteryReceiver;
private int batteryCapacity = 5400; // 电池容量mAh
private float lastBatteryPercent = 100.0f;
private long lastCheckTime = System.currentTimeMillis();
private EditText etSearch;
// 缓存相关
private Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
private PackageManager mPackageManager;
@Override
public Activity getActivity() {
return this;
}
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期方法 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_battery_report);
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化开始");
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
// 初始化UI组件
initView();
// 初始化PackageManager
mPackageManager = getPackageManager();
// 权限检查Java7 传统条件判断)
if (!hasUsageStatsPermission(this)) {
Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
LogUtils.w(TAG, "【onCreate】缺少使用情况访问权限引导用户开启");
return;
}
etSearch = (EditText) findViewById(R.id.et_search);
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
// 初始化流程新增“加载24小时累计耗电”步骤
// 初始化数据流程:加载应用→缓存名称→获取运行时长→计算初始累计耗电
loadAllAppPackage();
preCacheAllAppNames();
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电
calculateInitial24hTotalConsumption();
filteredList.addAll(dataList);
// 初始化适配器
adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
rvBatteryReport.setAdapter(adapter);
LogUtils.d(TAG, "【onCreate】适配器初始化完成数据量" + filteredList.size());
// 搜索监听(不变)
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
// 绑定搜索监听
bindSearchListener();
// 注册电池广播
registerBatteryReceiver();
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
filterAppsByPackageAndName(s.toString());
}
@Override
public void afterTextChanged(Editable s) {}
});
// 电池广播:调用修改后的“单次耗电计算+累计累加”方法
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra("level", 100);
int scale = intent.getIntExtra("scale", 100);
float currentPercent = (float) level / scale * 100;
LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
if (currentPercent < lastBatteryPercent) {
float dropPercent = lastBatteryPercent - currentPercent;
long duration = System.currentTimeMillis() - lastCheckTime;
LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms");
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); // 单次+累计逻辑
}
lastBatteryPercent = currentPercent;
lastCheckTime = System.currentTimeMillis();
}
};
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
LogUtils.d(TAG, "【onCreate】BatteryReportActivity 初始化完成");
}
@Override
@@ -149,33 +120,133 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
// Java7 显式非空判断
if (batteryReceiver != null) {
unregisterReceiver(batteryReceiver);
LogUtils.d(TAG, "【onDestroy】电池广播已注销");
}
LogUtils.d(TAG, "【onDestroy】BatteryReportActivity 销毁完成");
}
// ======================== UI初始化方法 =========================
private void initView() {
// 初始化Toolbar
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【导航栏】点击返回");
finish();
}
});
// 初始化RecyclerView与搜索框
etSearch = (EditText) findViewById(R.id.et_search);
rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);
rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
// ======================== 搜索监听绑定方法 =========================
private void bindSearchListener() {
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
LogUtils.d(TAG, "【bindSearchListener】搜索关键词变化" + s.toString());
filterAppsByPackageAndName(s.toString());
}
@Override
public void afterTextChanged(Editable s) {}
});
LogUtils.d(TAG, "【bindSearchListener】搜索监听绑定完成");
}
// ======================== 电池广播注册方法 =========================
private void registerBatteryReceiver() {
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra("level", 100);
int scale = intent.getIntExtra("scale", 100);
float currentPercent = (float) level / scale * 100;
LogUtils.d(TAG, "【电池广播】电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
if (currentPercent < lastBatteryPercent) {
float dropPercent = lastBatteryPercent - currentPercent;
long duration = System.currentTimeMillis() - lastCheckTime;
LogUtils.d(TAG, "【电池广播】电池消耗:" + dropPercent + "%,时长:" + formatRunTime(duration));
appRunTimeCache = getAppRunTime();
updateAppRunTimeToModel();
calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache);
}
lastBatteryPercent = currentPercent;
lastCheckTime = System.currentTimeMillis();
}
};
registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
LogUtils.d(TAG, "【registerBatteryReceiver】电池广播注册完成");
}
// ======================== 权限检查方法 =========================
/**
* 加载所有应用仅获取包名初始化模型时单次耗电、累计耗电均设为0
* 检查是否拥有使用情况访问权限
* @param context 上下文
* @return 拥有权限返回true否则返回false
*/
private boolean hasUsageStatsPermission(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LogUtils.w(TAG, "【hasUsageStatsPermission】系统版本低于LOLLIPOP不支持使用情况访问权限");
return false;
}
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if (manager == null) {
LogUtils.e(TAG, "【hasUsageStatsPermission】获取UsageStatsManager失败");
return false;
}
long endTime = System.currentTimeMillis();
long startTime = endTime - ONE_MINUTE_MS;
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
boolean hasPermission = statsList != null && !statsList.isEmpty();
LogUtils.d(TAG, "【hasUsageStatsPermission】使用情况访问权限检查结果" + hasPermission);
return hasPermission;
}
// ======================== 数据加载与缓存方法 =========================
/**
* 加载所有应用包名,初始化数据模型
*/
private void loadAllAppPackage() {
List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
dataList.clear();
LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用");
LogUtils.d(TAG, "【loadAllAppPackage】开始加载应用包名列表,共找到" + appList.size() + "个应用");
for (ApplicationInfo appInfo : appList) {
String packageName = appInfo.packageName;
// 初始化:单次耗电consumption=0累计耗电totalConsumption=0运行时长=0
// 初始化:单次耗电=0累计耗电=0运行时长=0
dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
}
LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名");
LogUtils.d(TAG, "【loadAllAppPackage】应用包名列表加载完成,共添加" + dataList.size() + "个包名");
}
/**
* 预缓存应用名称(逻辑不变)
* 预缓存所有应用名称减少PackageManager重复调用
*/
private void preCacheAllAppNames() {
packageToAppNameCache.clear();
LogUtils.d(TAG, "开始预缓存包名-应用名称映射");
LogUtils.d(TAG, "【preCacheAllAppNames】开始预缓存包名-应用名称映射");
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
@@ -183,48 +254,78 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
packageToAppNameCache.put(packageName, appName);
}
LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
LogUtils.d(TAG, "【preCacheAllAppNames】预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
}
/**
* 通过包名获取应用名称(逻辑不变)
* 通过包名获取应用名称,带异常处理
* @param packageName 应用包名
* @return 应用名称,获取失败返回包名
*/
private String getAppNameByPackage(String packageName) {
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
return mPackageManager.getApplicationLabel(appInfo).toString();
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage());
LogUtils.e(TAG, "【getAppNameByPackage】包名" + packageName + "对应的应用未找到:" + e.getMessage());
return packageName;
} catch (Exception e) {
LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "" + e.getMessage());
LogUtils.e(TAG, "【getAppNameByPackage】查询应用名称失败(包名:" + packageName + "" + e.getMessage());
return packageName;
}
}
/**
* 更新运行时长到模型(逻辑不变)
* 更新运行时长到数据模型
*/
private void updateAppRunTimeToModel() {
int nCount = 0;
int updateCount = 0;
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
Long runTime;
if (appRunTimeCache.containsKey(packageName)) {
runTime = appRunTimeCache.get(packageName);
LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName));
nCount++;
} else {
runTime = 0L;
}
Long runTime = appRunTimeCache.containsKey(packageName) ? appRunTimeCache.get(packageName) : 0L;
model.setRunTime(runTime);
if (runTime > 0) {
updateCount++;
}
}
LogUtils.d(TAG, String.format("dataList.size() %d appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size()));
LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount));
LogUtils.d(TAG, "【updateAppRunTimeToModel】更新完成数据量" + dataList.size() + ",更新运行时长应用数:" + updateCount);
}
/**
* 【新增】初始化时计算24小时累计耗电赋值给totalConsumption
* 获取应用24小时运行时长
* @return 应用包名-运行时长ms映射
*/
private Map<String, Long> getAppRunTime() {
Map<String, Long> runTimeMap = new HashMap<String, Long>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long endTime = System.currentTimeMillis();
long startTime = endTime - ONE_DAY_MS; // 近24小时
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
for (android.app.usage.UsageStats stats : statsList) {
long runTimeMs = stats.getTotalTimeInForeground();
String packageName = stats.getPackageName();
runTimeMap.put(packageName, runTimeMs);
LogUtils.v(TAG, "【getAppRunTime】包名" + packageName + "24小时运行时长" + formatRunTime(runTimeMs));
if (packageName.equals("aidepro.top")) {
LogUtils.d(TAG, "【getAppRunTime】特殊查询包名" + packageName + "有结果");
}
}
} catch (Exception e) {
LogUtils.e(TAG, "【getAppRunTime】获取应用运行时长失败" + e.getMessage());
}
}
LogUtils.d(TAG, "【getAppRunTime】应用运行时长列表数量" + runTimeMap.size());
return runTimeMap;
}
// ======================== 核心计算方法 =========================
/**
* 初始化时计算24小时累计耗电赋值给totalConsumption
* 逻辑基于24小时运行时长占比分配当前电池容量的理论24小时消耗
*/
private void calculateInitial24hTotalConsumption() {
@@ -233,23 +334,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
total24hRunTime += entry.getValue();
}
LogUtils.d(TAG, "24小时内所有应用总运行时长" + formatRunTime(total24hRunTime));
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时内所有应用总运行时长" + formatRunTime(total24hRunTime));
// 2. 按运行时长占比分配24小时累计耗电假设电池满电循环用总容量近似24小时总消耗
// 2. 按运行时长占比分配24小时累计耗电
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
// 计算占比与累计耗电
float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗
model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电
LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化%.1f mAh", packageName, initialTotalConsumption));
float initialTotalConsumption = batteryCapacity * ratio;
model.setTotalConsumption(initialTotalConsumption);
LogUtils.v(TAG, "【calculateInitial24hTotalConsumption】应用包" + packageName + "24小时累计耗电初始化" + initialTotalConsumption + " mAh");
}
LogUtils.d(TAG, "【calculateInitial24hTotalConsumption】24小时累计耗电初始化完成");
}
/**
* 【核心修改】计算单次耗电赋值给consumption+ 累加至累计耗电totalConsumption = totalConsumption + consumption
* 计算单次耗电赋值给consumption+ 累加至累计耗电totalConsumption = totalConsumption + consumption
* @param dropPercent 电池下降百分比
* @param runTimeMap 应用运行时长映射
*/
private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
long totalSingleRunTime = 0;
@@ -257,25 +361,26 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
totalSingleRunTime += entry.getValue();
}
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】本次电池下降总运行时长" + formatRunTime(totalSingleRunTime));
// 2. 遍历计算每个应用的“单次耗电”并“累加至累计”
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
// 步骤1计算本次单次耗电赋值给consumption
// 步骤1计算本次单次耗电
float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗
model.setConsumption(singleConsumption); // 存储单次耗电
float singleConsumption = batteryCapacity * dropPercent / 100 * ratio;
model.setConsumption(singleConsumption);
// 步骤2累加单次耗电到累计耗电totalConsumption = 原有累计 + 本次单次)
// 步骤2累加单次耗电到累计耗电
float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
model.setTotalConsumption(newTotalConsumption); // 更新累计耗电
model.setTotalConsumption(newTotalConsumption);
// 同步运行时长
model.setRunTime(appSingleRunTime);
LogUtils.d(TAG, String.format("应用包 %s单次耗电%.1f mAh累计耗电%.1f mAh",
LogUtils.v(TAG, String.format("【calculateSingleConsumptionAndAccumulate】应用包%s单次耗电%.1f mAh累计耗电%.1f mAh",
packageName, singleConsumption, newTotalConsumption));
}
@@ -289,69 +394,43 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
// 4. 重新应用过滤并刷新列表
filterAppsByPackageAndName(etSearch.getText().toString());
LogUtils.d(TAG, "【calculateSingleConsumptionAndAccumulate】单次耗电计算与累加完成列表已刷新");
}
/**
* 双维度过滤(逻辑不变
* 双维度过滤(包名+应用名
* @param keyword 搜索关键词
*/
private void filterAppsByPackageAndName(String keyword) {
filteredList.clear();
if (keyword == null || keyword.isEmpty()) {
filteredList.addAll(dataList);
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词为空显示全部应用数量" + filteredList.size());
} else {
String lowerKeyword = keyword.toLowerCase();
for (AppBatteryModel model : dataList) {
String packageName = model.getPackageName();
String packageNameLower = packageName.toLowerCase();
String appName = packageToAppNameCache.get(packageName);
String appNameLower = appName.toLowerCase();
boolean isMatched = packageNameLower.contains(lowerKeyword)
|| appNameLower.contains(lowerKeyword);
boolean isMatched = packageNameLower.contains(lowerKeyword)
|| appNameLower.contains(lowerKeyword);
if (isMatched) {
filteredList.add(model);
}
}
LogUtils.d(TAG, "【filterAppsByPackageAndName】搜索关键词" + keyword + ",匹配应用数量:" + filteredList.size());
}
adapter.notifyDataSetChanged();
}
// ======================== 工具方法 =========================
/**
* 获取应用运行时长逻辑不变返回24小时运行时长
*/
private Map<String, Long> getAppRunTime() {
Map<String, Long> runTimeMap = new HashMap<String, Long>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try {
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long endTime = System.currentTimeMillis();
long startTime = endTime - 24 * 3600 * 1000; // 近24小时
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
for (android.app.usage.UsageStats stats : statsList) {
long runTimeMs = stats.getTotalTimeInForeground();
String packageName = stats.getPackageName();
LogUtils.d(TAG, "包名" + packageName + "24小时运行时长" + formatRunTime(runTimeMs));
runTimeMap.put(packageName, runTimeMs);
if (packageName.equals("aidepro.top")) {
LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName));
}
}
} catch (Exception e) {
LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage());
}
}
LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size()));
return runTimeMap;
}
/**
* 格式化运行时长(逻辑不变)
* 格式化运行时长
* @param runTimeMs 运行时长ms
* @return 格式化后的运行时长字符串
*/
private String formatRunTime(long runTimeMs) {
if (runTimeMs <= 0) {
@@ -371,66 +450,47 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
}
}
// ======================== 内部类:数据模型 =========================
/**
* 权限检查(逻辑不变)
*/
private boolean hasUsageStatsPermission(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
android.app.usage.UsageStatsManager manager =
(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if (manager == null) {
return false;
}
long endTime = System.currentTimeMillis();
long startTime = endTime - 1000 * 60;
List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
return statsList != null && !statsList.isEmpty();
}
/**
* 【核心修改】数据模型:明确字段含义
* - consumption单次耗电两次电池广播间的消耗float类型便于计算
* - totalConsumption累计耗电24小时初始化值+后续单次累加,显示用)
* 应用电池数据模型
* - consumption单次耗电两次电池广播间的消耗
* - totalConsumption累计耗电24小时初始化值+后续单次累加)
* - runTime运行时长ms
* - packageName应用包名
*/
public static class AppBatteryModel {
private String packageName; // 应用包名(核心标识)
private float consumption; // 单次耗电mAhfloat类型
private float totalConsumption;// 累计耗电mAh,显示+排序用
private float consumption; // 单次耗电mAh
private float totalConsumption;// 累计耗电mAh
private long runTime; // 运行时长ms
// Java7 显式构造初始化单次耗电、累计耗电为0
// Java7 显式构造
public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
this.packageName = packageName;
this.consumption = consumption; // 单次耗电初始为0
this.totalConsumption = totalConsumption; // 累计耗电初始为0后续初始化时赋值
this.consumption = consumption;
this.totalConsumption = totalConsumption;
this.runTime = runTime;
}
// Getter/Setter:覆盖所有字段,确保数据操作正常
// Getter/Setter
public String getPackageName() {
return packageName;
}
public float getConsumption() {
return consumption; // 获取单次耗电
return consumption;
}
public void setConsumption(float consumption) {
this.consumption = consumption; // 设置单次耗电
this.consumption = consumption;
}
public float getTotalConsumption() {
return totalConsumption; // 获取累计耗电(显示用)
return totalConsumption;
}
public void setTotalConsumption(float totalConsumption) {
this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用)
this.totalConsumption = totalConsumption;
}
public long getRunTime() {
@@ -442,8 +502,9 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
}
}
// ======================== 内部类RecyclerView适配器 =========================
/**
* RecyclerView 适配器仅显示累计耗电totalConsumption逻辑适配模型修改
* 电池报告列表适配器,显示应用名称、累计耗电、运行时长
*/
public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
private Context mContext;
@@ -451,8 +512,8 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
private PackageManager mPm;
private Map<String, String> mPackageToNameCache;
// Java7 显式构造:接收名称缓存,确保显示时高效获取应用名
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
// Java7 显式构造
public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList,
PackageManager pm, Map<String, String> packageToNameCache) {
this.mContext = context;
this.mDataList = dataList;
@@ -462,18 +523,18 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 加载系统列表项布局text1显示应用名text2显示累计耗电+时长)
View itemView = LayoutInflater.from(mContext)
.inflate(android.R.layout.simple_list_item_2, parent, false);
.inflate(android.R.layout.simple_list_item_2, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Java7 显式非空判断:避免空指针异常
// Java7 显式非空判断
if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
holder.tvAppName.setText("未知应用");
holder.tvConsumption.setText("累计耗电0.0 mAh | 运行时长0秒");
LogUtils.w(TAG, "【onBindViewHolder】数据异常位置" + position);
return;
}
@@ -481,11 +542,11 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
String packageName = model.getPackageName();
String appName = "";
// 优先从缓存获取应用名减少PackageManager调用提升性能
// 优先从缓存获取应用名
if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
appName = mPackageToNameCache.get(packageName);
} else {
// 缓存无数据时兜底查询,并同步更新缓存
// 缓存无数据时兜底查询
try {
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
appName = mPm.getApplicationLabel(appInfo).toString();
@@ -493,45 +554,40 @@ public class BatteryReportActivity extends WinBoLLActivity implements IWinBoLLAc
mPackageToNameCache.put(packageName, appName);
}
} catch (PackageManager.NameNotFoundException e) {
appName = packageName; // 包名不存在时用包名兜底
LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage());
appName = packageName;
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】包名" + packageName + "对应的应用未找到:" + e.getMessage());
} catch (Exception e) {
appName = packageName; // 其他异常时用包名兜底
LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "" + e.getMessage());
appName = packageName;
LogUtils.e("BatteryReportAdapter", "【onBindViewHolder】查询应用名称失败(包名:" + packageName + "" + e.getMessage());
}
}
// 显示逻辑:仅展示累计耗电totalConsumption隐藏单次耗电
// 显示逻辑:应用名称 + 累计耗电 + 运行时长
holder.tvAppName.setText(appName);
// 格式化运行时长 + 累计耗电保留1位小数提升可读性
String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
model.getTotalConsumption(), runTimeStr);
holder.tvConsumption.setText(totalConsumptionText);
// 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改)
// 显示优化
holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
// 调整文字大小:适配手机屏幕,提升可读性
holder.tvAppName.setTextSize(16);
holder.tvConsumption.setTextSize(14);
}
// 获取列表长度Java7 三元运算符判断空值,避免空指针
@Override
public int getItemCount() {
return mDataList == null ? 0 : mDataList.size();
}
/**
* ViewHolder绑定系统布局控件,与显示逻辑对应
* ViewHolder绑定系统布局控件
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvAppName; // 显示应用名称
TextView tvConsumption; // 显示累计耗电 + 运行时长
TextView tvAppName; // 应用名称
TextView tvConsumption; // 累计耗电 + 运行时长
// Java7 显式构造绑定控件ID系统布局固定IDtext1、text2
public ViewHolder(View itemView) {
super(itemView);
tvAppName = (TextView) itemView.findViewById(android.R.id.text1);

View File

@@ -6,8 +6,9 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
@@ -17,39 +18,68 @@ import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;
import java.util.ArrayList;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
/**
* 电池记录清理页面,支持滑动清理记录、切换记录显示格式
* 适配 API30基于 Java7 开发
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
*/
public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActivity {
// ======================== 静态常量 =========================
public static final String TAG = "ClearRecordActivity";
private static final String TOAST_MSG_CLEAR_SUCCESS = "The APP battery record is cleaned.";
// ======================== 成员变量 =========================
// UI组件
private Toolbar mToolbar;
TextView mtvRecordText;
App mApplication;
boolean mIsShowRecordWithEnter = false;
private TextView mtvRecordText;
private TextView tvAOHPCTCSeekBarMSG;
private AOHPCTCSeekBar aOHPCTCSeekBar;
// 应用与配置
private App mApplication;
private boolean mIsShowRecordWithEnter = false; // 记录是否带换行显示
@Override
public Activity getActivity() {
return this;
}
// ======================== 接口实现方法 =========================
@Override
public Activity getActivity() {
return this;
}
@Override
public String getTag() {
return TAG;
}
@Override
public String getTag() {
return TAG;
}
// ======================== 生命周期方法 =========================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clearrecord);
mApplication = (App) getApplication();
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化开始");
// 初始化工具栏
mToolbar = findViewById(R.id.toolbar);
// 初始化应用实例
mApplication = (App) getApplication();
// 初始化UI组件
initView();
// 初始化滑动清理控件
initSeekBar();
// 初始化记录显示文本
initRecordText();
LogUtils.d(TAG, "【onCreate】ClearRecordActivity 初始化完成");
}
// ======================== UI初始化方法 =========================
/**
* 初始化Toolbar与显示文本组件
*/
private void initView() {
// 初始化Toolbar
mToolbar = findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
mToolbar.setSubtitle(getTag());
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -58,49 +88,76 @@ public class ClearRecordActivity extends WinBoLLActivity implements IWinBoLLActi
}
});
// 设置滑动清理控
//
// 初始化发送拉动控件
final AOHPCTCSeekBar aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
// 初始化显示文本组
tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
LogUtils.d(TAG, "【initView】UI组件初始化完成");
}
/**
* 初始化滑动清理控件
*/
private void initSeekBar() {
aOHPCTCSeekBar = findViewById(R.id.activityclearrecordAOHPCTCSeekBar1);
aOHPCTCSeekBar.setThumb(getDrawable(R.drawable.cursor_pointer));
aOHPCTCSeekBar.setThumbOffset(0);
aOHPCTCSeekBar.setOnOHPCListener(
new AOHPCTCSeekBar.OnOHPCListener(){
@Override
public void onOHPCommit() {
mApplication.clearBatteryHistory();
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
initRecordText();
String szMSG = "The APP battery record is cleaned.";
LogUtils.d(TAG, szMSG);
ToastUtils.show(szMSG);
}
});
// 初始化提示框
TextView tvAOHPCTCSeekBarMSG = findViewById(R.id.activityclearrecordTextView1);
tvAOHPCTCSeekBarMSG.setText(R.string.msg_AOHPCTCSeekBar_ClearRecord);
mtvRecordText = findViewById(R.id.activityclearrecordTextView2);
initRecordText();
aOHPCTCSeekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
@Override
public void onOHPCommit() {
LogUtils.d(TAG, "【onOHPCommit】滑动清理触发");
// 清理电池历史记录
mApplication.clearBatteryHistory();
// 发送广播更新前台通知
sendBroadcast(new Intent(ControlCenterServiceReceiver.ACTION_UPDATE_FOREGROUND_NOTIFICATION));
// 刷新记录显示
initRecordText();
// 提示清理成功
ToastUtils.show(TOAST_MSG_CLEAR_SUCCESS);
LogUtils.d(TAG, "【onOHPCommit】电池记录清理完成已发送更新广播");
}
});
LogUtils.d(TAG, "【initSeekBar】滑动清理控件初始化完成");
}
// ======================== 业务逻辑方法 =========================
/**
* 初始化记录显示文本,根据配置切换带换行/不带换行格式
*/
void initRecordText() {
ArrayList<BatteryInfoBean> listBatteryInfo = AppCacheUtils.getInstance(this).getArrayListBatteryInfo();
if (mIsShowRecordWithEnter) {
String szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
mtvRecordText.setText(szRecordText);
} else {
String szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
mtvRecordText.setText(szRecordText);
}
String szRecordText;
// 判空处理:避免空列表导致异常
if (listBatteryInfo == null || listBatteryInfo.isEmpty()) {
szRecordText = getString(R.string.msg_no_battery_record);
LogUtils.d(TAG, "【initRecordText】无电池记录数据");
} else {
// 根据配置切换显示格式
if (mIsShowRecordWithEnter) {
szRecordText = StringUtils.formatPCMListStringWithEnter(listBatteryInfo);
LogUtils.d(TAG, "【initRecordText】使用带换行格式显示记录数量" + listBatteryInfo.size());
} else {
szRecordText = StringUtils.formatPCMListString(listBatteryInfo);
LogUtils.d(TAG, "【initRecordText】使用无换行格式显示记录数量" + listBatteryInfo.size());
}
}
mtvRecordText.setText(szRecordText);
LogUtils.d(TAG, "【initRecordText】记录显示文本初始化完成");
}
public void onShowRecordWithEnter(View view) {
Switch swShowRecordWithEnter = (Switch)view;
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
initRecordText();
}
// ======================== 事件回调方法 =========================
/**
* 切换记录显示格式(带换行/不带换行)
* @param view 触发事件的Switch控件
*/
public void onShowRecordWithEnter(View view) {
Switch swShowRecordWithEnter = (Switch) view;
mIsShowRecordWithEnter = swShowRecordWithEnter.isChecked();
LogUtils.d(TAG, "【onShowRecordWithEnter】记录显示格式切换带换行" + mIsShowRecordWithEnter);
// 刷新记录显示
initRecordText();
}
}

View File

@@ -16,9 +16,10 @@ public class BatteryUtils {
public static final String TAG = "BatteryUtils";
// 电池电量计算常量
private static final int BATTERY_SCALE_DEFAULT = 100;
private static final int BATTERY_LEVEL_MIN = 0;
private static final int BATTERY_LEVEL_MAX = 100;
private static final int BATTERY_SCALE_DEFAULT = 100; // 电量刻度默认值
private static final int BATTERY_LEVEL_MIN = 0; // 电量百分比最小值
private static final int BATTERY_LEVEL_MAX = 100; // 电量百分比最大值
private static final int EXTRA_STATUS_DEFAULT = -1; // 电池状态默认值
// ================================== 工具方法(静态方法,无状态设计)=================================
/**
@@ -27,16 +28,21 @@ public class BatteryUtils {
* @return true=充电中/已充满false=未充电
*/
public static boolean isCharging(Intent intent) {
LogUtils.d(TAG, "isCharging: 调用 | intent=" + intent);
LogUtils.d(TAG, "isCharging】调用开始");
// 入参非空校验
if (intent == null) {
LogUtils.e(TAG, "isCharging: intent为空返回false");
LogUtils.e(TAG, "isCharging】入参异常:intent为空返回false");
return false;
}
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
LogUtils.d(TAG, "isCharging: 解析完成 | status=" + status + " | result=" + isCharging);
// 解析电池状态
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, EXTRA_STATUS_DEFAULT);
LogUtils.d(TAG, "isCharging】解析电池状态:status=" + status);
// 判断充电状态(充电中/已充满均视为充电状态)
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL;
LogUtils.d(TAG, "【isCharging】调用结束 | 充电状态=" + isCharging);
return isCharging;
}
@@ -46,29 +52,30 @@ public class BatteryUtils {
* @return 电量百分比异常返回0
*/
public static int getCurrentBatteryLevel(Intent intent) {
LogUtils.d(TAG, "getCurrentBatteryLevel: 调用 | intent=" + intent);
LogUtils.d(TAG, "getCurrentBatteryLevel】调用开始");
// 入参非空校验
if (intent == null) {
LogUtils.e(TAG, "getCurrentBatteryLevel: intent为空返回0");
LogUtils.e(TAG, "getCurrentBatteryLevel】入参异常:intent为空返回0");
return BATTERY_LEVEL_MIN;
}
// 解析电量原始值与刻度值
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, BATTERY_LEVEL_MIN);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE_DEFAULT);
LogUtils.d(TAG, "getCurrentBatteryLevel: 原始值 | level=" + level + " | scale=" + scale);
LogUtils.d(TAG, "getCurrentBatteryLevel】解析原始数据 | level=" + level + " | scale=" + scale);
// 计算并校验电量百分比避免除以0或数值越界
int batteryLevel;
if (scale <= 0) {
LogUtils.w(TAG, "【getCurrentBatteryLevel】刻度值无效scale=" + scale + "直接使用level值");
batteryLevel = level;
LogUtils.w(TAG, "getCurrentBatteryLevel: scale无效直接使用level值");
} else {
batteryLevel = level * BATTERY_SCALE_DEFAULT / scale;
}
// 确保电量值在0-100范围内
batteryLevel = Math.max(BATTERY_LEVEL_MIN, Math.min(batteryLevel, BATTERY_LEVEL_MAX));
LogUtils.d(TAG, "getCurrentBatteryLevel: 计算完成 | batteryLevel=" + batteryLevel + "%");
LogUtils.d(TAG, "getCurrentBatteryLevel】调用结束 | 电量百分比=" + batteryLevel + "%");
return batteryLevel;
}
}

View File

@@ -2,6 +2,7 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
@@ -23,26 +24,34 @@ import java.util.concurrent.ConcurrentHashMap;
* 核心策略无论内存如何紧张强制保持已缓存的Bitmap保留图片原始品质永不自动清理
*/
public class BitmapCacheUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "BitmapCacheUtils";
// 移除最大图片尺寸限制(不压缩图片)
// 若需限制尺寸而非压缩品质,可保留此常量用于校验,不参与采样率计算
// SP 相关常量
private static final String SP_NAME = "BitmapCacheSP";
private static final String SP_KEY_LAST_CACHE_PATH = "last_cache_image_path";
// 单例实例volatile 保证多线程可见性)
// Bitmap 解码常量
private static final int BITMAP_SAMPLE_SIZE_ORIGINAL = 1; // 无压缩采样率
private static final Bitmap.Config BITMAP_CONFIG_DEFAULT = Bitmap.Config.ARGB_8888; // 全彩品质配置
// ================================== 成员变量按功能分类volatile 保证多线程可见性)=================================
// 单例实例
private static volatile BitmapCacheUtils sInstance;
// 路径-Bitmap 硬引用缓存(唯一缓存,极致强制保持,任何情况不自动回收)
// 路径-Bitmap 硬引用缓存(极致强制保持,不自动回收)
private final Map<String, Bitmap> mHardCacheMap;
// 路径-引用计数 映射(解决多实例共享问题,仅用于统计,不影响缓存生命周期)
// 路径-引用计数 映射(统计,不影响缓存生命周期)
private final Map<String, Integer> mRefCountMap;
// SP 实例(用于持久化最后缓存路径)
private final SharedPreferences mSp;
// 私有构造器(单例模式)
// ================================== 单例方法(双重校验锁,线程安全)=================================
/**
* 私有构造器(单例模式)
*/
private BitmapCacheUtils() {
// 使用ConcurrentHashMap保证线程安全避免手动同步
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造开始");
// 使用 ConcurrentHashMap 保证线程安全,避免手动同步
mHardCacheMap = new ConcurrentHashMap<>();
mRefCountMap = new ConcurrentHashMap<>();
// 初始化 SP使用 App 全局上下文,避免内存泄漏)
@@ -51,6 +60,7 @@ public class BitmapCacheUtils {
preloadLastCachedBitmap();
// 注册内存状态监听(仅记录日志,不清理缓存)
registerMemoryStatusListener();
LogUtils.d(TAG, "【BitmapCacheUtils】单例构造完成极致强制缓存策略已启用");
}
/**
@@ -67,14 +77,14 @@ public class BitmapCacheUtils {
return sInstance;
}
// ====================================== 保留核心监控方法保证与App类兼容 ======================================
// ================================== 对外监控接口App 类调用专用)=================================
/**
* 获取当前缓存的Bitmap数量App类调用专用
* @return 缓存的Bitmap数量
* 获取当前缓存的 Bitmap 数量
* @return 缓存的 Bitmap 数量
*/
public int getCacheCount() {
int count = mHardCacheMap.size();
LogUtils.d(TAG, "getCacheCount: 当前缓存Bitmap数量 - " + count);
LogUtils.d(TAG, "getCacheCount当前缓存 Bitmap 数量 - " + count);
return count;
}
@@ -83,7 +93,9 @@ public class BitmapCacheUtils {
* @return 路径集合
*/
public Set<String> getCachedPaths() {
return mHardCacheMap.keySet();
Set<String> paths = mHardCacheMap.keySet();
LogUtils.d(TAG, "【getCachedPaths】当前缓存路径数量 - " + paths.size());
return paths;
}
/**
@@ -101,20 +113,22 @@ public class BitmapCacheUtils {
}
}
}
LogUtils.d(TAG, "getTotalCacheSize: 当前缓存总内存占用 - " + totalSize + " 字节");
LogUtils.d(TAG, "getTotalCacheSize当前缓存总内存占用 - " + totalSize + " 字节");
return totalSize;
}
// ====================================== 核心接口:缓存操作(无压缩) ======================================
// ================================== 对外核心接口:缓存操作(无压缩)=================================
/**
* 补充接口:直接缓存已解码的Bitmap适配BackgroundView改进需求
* 直接缓存已解码的 Bitmap适配 BackgroundView 改进需求)
* @param imagePath 图片绝对路径
* @param bitmap 已解码的有效Bitmap
* @return 缓存后的Bitmap / null参数无效
* @param bitmap 已解码的有效 Bitmap
* @return 缓存后的 Bitmap / null参数无效
*/
public Bitmap cacheBitmap(String imagePath, Bitmap bitmap) {
LogUtils.d(TAG, "【cacheBitmap】调用开始直接缓存已解码 Bitmap| 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath) || !isBitmapValid(bitmap)) {
LogUtils.e(TAG, "cacheBitmap: 路径或Bitmap无效");
LogUtils.e(TAG, "cacheBitmap】入参异常:路径为空或 Bitmap 无效");
return null;
}
@@ -124,35 +138,39 @@ public class BitmapCacheUtils {
mRefCountMap.putIfAbsent(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 直接缓存已解码Bitmap成功(极致强制保持,无压缩) - " + imagePath);
LogUtils.d(TAG, "cacheBitmap】调用成功(直接缓存已解码 Bitmap| 路径=" + imagePath);
return bitmap;
}
/**
* 核心接口:根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP
* 根据图片路径缓存 Bitmap 到内存,并持久化路径到 SP
* @param imagePath 图片绝对路径
* @return 缓存成功的 Bitmap / null路径无效/文件不存在/解码失败)
*/
public Bitmap cacheBitmap(String imagePath) {
LogUtils.d(TAG, "【cacheBitmap】调用开始路径缓存| 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "cacheBitmap: 图片路径为空");
LogUtils.e(TAG, "cacheBitmap】入参异常:图片路径为空");
return null;
}
// 文件有效性校验
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "cacheBitmap: 图片文件无效不存在/非文件/空文件 - " + imagePath);
LogUtils.e(TAG, "cacheBitmap文件无效不存在/非文件/空文件 | 路径=" + imagePath);
return null;
}
// 已缓存则直接返回,避免重复加载
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
if (isBitmapValid(hardCacheBitmap)) {
LogUtils.d(TAG, "cacheBitmap: 硬引用缓存命中,引用计数+1 - " + imagePath);
LogUtils.d(TAG, "cacheBitmap硬引用缓存命中,引用计数+1 | 路径=" + imagePath);
// 引用计数+1
increaseRefCount(imagePath);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "【cacheBitmap】调用成功缓存命中| 路径=" + imagePath);
return hardCacheBitmap;
}
@@ -165,41 +183,47 @@ public class BitmapCacheUtils {
mRefCountMap.put(imagePath, 1);
// 持久化当前路径到 SP
saveLastCachePathToSp(imagePath);
LogUtils.d(TAG, "cacheBitmap: 图片缓存成功并持久化路径(极致强制保持,无压缩) - " + imagePath);
LogUtils.d(TAG, "cacheBitmap】调用成功(新缓存)| 路径=" + imagePath);
} else {
LogUtils.e(TAG, "cacheBitmap: 图片解码失败 - " + imagePath);
LogUtils.e(TAG, "cacheBitmap】调用失败:图片解码失败 | 路径=" + imagePath);
}
return bitmap;
}
/**
* 核心接口:根据路径获取缓存的 Bitmap
* 根据路径获取缓存的 Bitmap
* @param imagePath 图片绝对路径
* @return 缓存的有效 Bitmap / null未缓存/已回收)
*/
public Bitmap getCachedBitmap(String imagePath) {
LogUtils.d(TAG, "【getCachedBitmap】调用开始 | 路径=" + imagePath);
// 入参非空校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【getCachedBitmap】入参异常图片路径为空");
return null;
}
// 仅从硬引用缓存获取,无任何 fallback
Bitmap hardCacheBitmap = mHardCacheMap.get(imagePath);
if (isBitmapValid(hardCacheBitmap)) {
LogUtils.d(TAG, "【getCachedBitmap】调用成功缓存命中| 路径=" + imagePath);
return hardCacheBitmap;
}
// 缓存未命中或Bitmap已失效极致强制策略下理论上不会出现已回收情况
LogUtils.w(TAG, "getCachedBitmap: 缓存未命中或Bitmap已失效 - " + imagePath);
// 缓存未命中或 Bitmap 已失效(极致强制策略下,理论上不会出现已回收情况)
LogUtils.w(TAG, "getCachedBitmap】调用失败:缓存未命中或 Bitmap 已失效 | 路径=" + imagePath);
return null;
}
// ====================================== 引用计数管理(仅统计,不影响缓存) ======================================
// ================================== 对外接口:引用计数管理(仅统计,不影响缓存)=================================
/**
* 新增接口:增加指定路径Bitmap的引用计数
* 增加指定路径 Bitmap 的引用计数
* @param imagePath 图片绝对路径
*/
public void increaseRefCount(String imagePath) {
LogUtils.d(TAG, "【increaseRefCount】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【increaseRefCount】入参异常图片路径为空");
return;
}
synchronized (mRefCountMap) {
@@ -209,44 +233,48 @@ public class BitmapCacheUtils {
} else {
mRefCountMap.put(imagePath, count + 1);
}
LogUtils.d(TAG, "increaseRefCount: " + imagePath + " 引用计数变为 " + mRefCountMap.get(imagePath));
int newCount = mRefCountMap.get(imagePath);
LogUtils.d(TAG, "【increaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount);
}
}
/**
* 新增接口:减少指定路径Bitmap的引用计数计数为0时仅标记不回收极致强制缓存策略
* 减少指定路径 Bitmap 的引用计数计数为0时仅标记不回收极致强制缓存策略
* @param imagePath 图片绝对路径
*/
public void decreaseRefCount(String imagePath) {
LogUtils.d(TAG, "【decreaseRefCount】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【decreaseRefCount】入参异常图片路径为空");
return;
}
synchronized (mRefCountMap) {
Integer count = mRefCountMap.get(imagePath);
if (count == null || count <= 0) {
LogUtils.w(TAG, "【decreaseRefCount】引用计数无效路径=" + imagePath);
return;
}
int newCount = count - 1;
if (newCount <= 0) {
// 极致强制缓存策略引用计数为0时仅移除计数绝对不回收Bitmap
// 极致强制缓存策略引用计数为0时仅移除计数绝对不回收 Bitmap
mRefCountMap.remove(imagePath);
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数为0极致强制保持Bitmap");
LogUtils.d(TAG, "decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数为0极致强制保持 Bitmap");
} else {
mRefCountMap.put(imagePath, newCount);
LogUtils.d(TAG, "decreaseRefCount: " + imagePath + " 引用计数变为 " + newCount);
LogUtils.d(TAG, "decreaseRefCount】调用成功 | 路径=" + imagePath + " | 引用计数=" + newCount);
}
}
}
// ====================================== 缓存清理(仅手动调用,永不自动执行) ======================================
// ================================== 对外接口:缓存清理(仅手动调用,永不自动执行)=================================
/**
* 清空所有 Bitmap 缓存(仅手动调用时执行,任何情况不自动执行)
*/
public void clearAllCache() {
LogUtils.w(TAG, "clearAllCache: 手动清空所有缓存(极致强制缓存策略下,需谨慎使用)");
LogUtils.w(TAG, "clearAllCache】调用开始(极致强制缓存策略下,需谨慎使用)");
// 清空硬引用缓存并回收Bitmap
// 清空硬引用缓存并回收 Bitmap
for (Bitmap bitmap : mHardCacheMap.values()) {
if (isBitmapValid(bitmap)) {
bitmap.recycle();
@@ -260,7 +288,7 @@ public class BitmapCacheUtils {
// 清空 SP 中保存的最后缓存路径
clearLastCachePathInSp();
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
LogUtils.d(TAG, "clearAllCache】调用成功:所有 Bitmap 缓存已清空");
}
/**
@@ -268,16 +296,18 @@ public class BitmapCacheUtils {
* @param imagePath 图片绝对路径
*/
public void removeCachedBitmap(String imagePath) {
LogUtils.d(TAG, "【removeCachedBitmap】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【removeCachedBitmap】入参异常图片路径为空");
return;
}
synchronized (mRefCountMap) {
// 手动移除时才回收Bitmap
// 手动移除时才回收 Bitmap
Bitmap hardBitmap = mHardCacheMap.remove(imagePath);
if (isBitmapValid(hardBitmap)) {
hardBitmap.recycle();
LogUtils.d(TAG, "removeCachedBitmap: 手动回收硬引用缓存 - " + imagePath);
LogUtils.d(TAG, "removeCachedBitmap手动回收硬引用缓存 | 路径=" + imagePath);
}
mRefCountMap.remove(imagePath);
@@ -285,22 +315,24 @@ public class BitmapCacheUtils {
String lastPath = getLastCachePathFromSp();
if (imagePath.equals(lastPath)) {
clearLastCachePathInSp();
LogUtils.d(TAG, "removeCachedBitmap: 移除的是最后缓存路径,已清空 SP");
LogUtils.d(TAG, "removeCachedBitmap】移除最后缓存路径,已清空 SP");
}
}
LogUtils.d(TAG, "【removeCachedBitmap】调用成功 | 路径=" + imagePath);
}
// ====================================== 内部工具方法(无压缩解码 ======================================
// ================================== 内部工具方法(无压缩解码 + Bitmap 有效性判断)=================================
/**
* 无压缩解码 Bitmap保留原始品质
* @param imagePath 图片绝对路径
* @return 解码后的 Bitmap / null文件无效/解码失败)
*/
private Bitmap decodeOriginalBitmap(String imagePath) {
LogUtils.d(TAG, "【decodeOriginalBitmap】调用开始 | 路径=" + imagePath);
// 前置校验:确保文件有效
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
LogUtils.e(TAG, "decodeOriginalBitmap: 文件无效,跳过解码 - " + imagePath);
LogUtils.e(TAG, "decodeOriginalBitmap文件无效,跳过解码 | 路径=" + imagePath);
return null;
}
@@ -311,47 +343,55 @@ public class BitmapCacheUtils {
// 校验尺寸是否有效
if (options.outWidth <= 0 || options.outHeight <= 0) {
LogUtils.e(TAG, "decodeOriginalBitmap: 图片尺寸无效 - " + imagePath);
LogUtils.e(TAG, "decodeOriginalBitmap图片尺寸无效 | 路径=" + imagePath);
return null;
}
LogUtils.d(TAG, "decodeOriginalBitmap: 图片原始尺寸 - " + options.outWidth + "x" + options.outHeight);
LogUtils.d(TAG, "decodeOriginalBitmap图片原始尺寸 | 宽=" + options.outWidth + " | 高=" + options.outHeight);
// 无压缩解码配置
options.inJustDecodeBounds = false;
options.inSampleSize = 1; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 保留全彩品质如需节省内存可改为RGB_565不影响品质
options.inSampleSize = BITMAP_SAMPLE_SIZE_ORIGINAL; // 不缩放采样率为1
options.inPreferredConfig = BITMAP_CONFIG_DEFAULT; // 保留全彩品质
options.inPurgeable = false; // 关闭可清除标志,极致强制保持内存
options.inInputShareable = false;
options.inDither = true; // 开启抖动,保证色彩还原
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
try {
return BitmapFactory.decodeFile(imagePath, options);
Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options);
LogUtils.d(TAG, "【decodeOriginalBitmap】解码" + (bitmap != null ? "成功" : "失败") + " | 路径=" + imagePath);
return bitmap;
} catch (OutOfMemoryError e) {
LogUtils.e(TAG, "decodeOriginalBitmap: OOM异常无压缩图片尺寸过大 - " + imagePath);
// 极致强制缓存策略OOM时仅放弃当前解码绝对不清理已缓存的Bitmap
LogUtils.e(TAG, "decodeOriginalBitmapOOM 异常(无压缩,图片尺寸过大)| 路径=" + imagePath);
// 极致强制缓存策略OOM 时仅放弃当前解码,绝对不清理已缓存的 Bitmap
return null;
} catch (Exception e) {
LogUtils.e(TAG, "decodeOriginalBitmap: 解码异常 - " + imagePath, e);
LogUtils.e(TAG, "decodeOriginalBitmap解码异常 | 路径=" + imagePath, e);
return null;
}
}
/**
* 工具方法:判断Bitmap是否有效非空且未被回收
* 判断 Bitmap 是否有效(非空且未被回收)
*/
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
boolean isValid = bitmap != null && !bitmap.isRecycled();
if (!isValid) {
LogUtils.w(TAG, "【isBitmapValid】Bitmap 无效:空或已回收");
}
return isValid;
}
// ====================================== SP 持久化相关 ======================================
// ================================== 内部工具方法:SP 持久化相关 ==================================
/**
* 从 SP 中获取最后一次缓存的图片路径
* @return 最后缓存的路径 / null未保存
*/
private String getLastCachePathFromSp() {
return mSp.getString(SP_KEY_LAST_CACHE_PATH, null);
String path = mSp.getString(SP_KEY_LAST_CACHE_PATH, null);
LogUtils.d(TAG, "【getLastCachePathFromSp】获取最后缓存路径 | 路径=" + path);
return path;
}
/**
@@ -359,11 +399,13 @@ public class BitmapCacheUtils {
* @param imagePath 图片绝对路径
*/
private void saveLastCachePathToSp(String imagePath) {
LogUtils.d(TAG, "【saveLastCachePathToSp】调用开始 | 路径=" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "【saveLastCachePathToSp】入参异常图片路径为空");
return;
}
mSp.edit().putString(SP_KEY_LAST_CACHE_PATH, imagePath).commit(); // Java 7 兼容,使用 commit 而非 apply
LogUtils.d(TAG, "saveLastCachePathToSp: 持久化最后缓存路径 - " + imagePath);
LogUtils.d(TAG, "saveLastCachePathToSp】调用成功 | 路径=" + imagePath);
}
/**
@@ -371,64 +413,42 @@ public class BitmapCacheUtils {
*/
private void clearLastCachePathInSp() {
mSp.edit().remove(SP_KEY_LAST_CACHE_PATH).commit();
LogUtils.d(TAG, "clearLastCachePathInSp: SP 中最后缓存路径已清空");
LogUtils.d(TAG, "clearLastCachePathInSp】调用成功:SP 中最后缓存路径已清空");
}
// ====================================== 预加载相关 ======================================
// ================================== 内部工具方法:预加载相关 ==================================
/**
* 构造时预加载 SP 中保存的最后一次缓存路径的图片
*/
private void preloadLastCachedBitmap() {
LogUtils.d(TAG, "【preloadLastCachedBitmap】调用开始");
String lastPath = getLastCachePathFromSp();
if (TextUtils.isEmpty(lastPath)) {
LogUtils.d(TAG, "preloadLastCachedBitmap: SP 中无保存的缓存路径,跳过预加载");
LogUtils.d(TAG, "preloadLastCachedBitmapSP 中无保存的缓存路径,跳过预加载");
return;
}
// 调用 cacheBitmap 预加载(内部已做文件校验和缓存判断)
Bitmap bitmap = cacheBitmap(lastPath);
if (bitmap != null) {
LogUtils.d(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径成功(极致强制保持,无压缩) - " + lastPath);
LogUtils.d(TAG, "preloadLastCachedBitmap预加载成功 | 路径=" + lastPath);
} else {
LogUtils.w(TAG, "preloadLastCachedBitmap: 预加载 SP 中最后缓存路径失败,清空无效路径 - " + lastPath);
LogUtils.w(TAG, "preloadLastCachedBitmap预加载失败,清空无效路径 | 路径=" + lastPath);
// 预加载失败,清空 SP 中无效路径
clearLastCachePathInSp();
}
}
// ====================================== 内存状态监听(仅记录日志) ======================================
// ================================== 内部工具方法:内存状态监听(仅记录日志)=================================
/**
* 注册内存状态监听(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private void registerMemoryStatusListener() {
LogUtils.d(TAG, "【registerMemoryStatusListener】调用开始");
if (Build.VERSION.SDK_INT >= 14) {
App.getInstance().registerComponentCallbacks(new MemoryStatusCallback());
LogUtils.d(TAG, "registerMemoryStatusListener: 内存状态监听已注册(仅记录日志,不清理缓存)");
}
}
/**
* 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private class MemoryStatusCallback implements android.content.ComponentCallbacks2 {
@Override
public void onTrimMemory(int level) {
// 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onTrimMemory: 内存紧张级别 - " + level + "极致强制保持所有Bitmap缓存无压缩");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onLowMemory() {
// 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "onLowMemory: 系统低内存极致强制保持所有Bitmap缓存无压缩");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
// 配置变化时无需处理
LogUtils.d(TAG, "registerMemoryStatusListener内存状态监听已注册(仅记录日志,不清理缓存)");
} else {
LogUtils.w(TAG, "【registerMemoryStatusListener】API 版本低于14不支持内存状态监听");
}
}
@@ -436,8 +456,35 @@ public class BitmapCacheUtils {
* 记录当前缓存状态(用于内存紧张时的调试)
*/
private void logCurrentCacheStatus() {
LogUtils.d(TAG, "logCurrentCacheStatus: 缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节");
LogUtils.d(TAG, "logCurrentCacheStatus: 缓存路径 - " + getCachedPaths().toString());
LogUtils.d(TAG, "logCurrentCacheStatus缓存数量 - " + getCacheCount() + ",总内存占用 - " + getTotalCacheSize() + " 字节");
LogUtils.d(TAG, "logCurrentCacheStatus缓存路径 - " + getCachedPaths().toString());
}
// ================================== 内部类:内存状态回调(仅记录日志)=================================
/**
* 内存状态回调(仅记录日志,不清理缓存,极致强制缓存策略)
*/
private class MemoryStatusCallback implements android.content.ComponentCallbacks2 {
@Override
public void onTrimMemory(int level) {
// 极致强制缓存策略:内存紧张时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "【onTrimMemory】内存紧张级别 - " + level + ",极致强制保持所有 Bitmap 缓存(无压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onLowMemory() {
// 极致强制缓存策略:低内存时仅记录日志,不清理任何缓存
LogUtils.w(TAG, "【onLowMemory】系统低内存极致强制保持所有 Bitmap 缓存(无压缩)");
// 记录当前缓存状态
logCurrentCacheStatus();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// 配置变化时无需处理
}
}
}

View File

@@ -1,16 +1,37 @@
package cc.winboll.studio.powerbell.utils;
import java.text.SimpleDateFormat;
import java.util.Locale;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/24
* @Describe 日期时间工具类Java 7 兼容 | API 30 适配)
* 功能:提供当前时间的格式化字符串获取功能
*/
public class DateUtils {
// 获取当前时间的格式化字符串
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "DateUtils";
private static final String DATE_FORMAT_PATTERN = "yyyyMMdd_HHmmssSSS"; // 修正年份格式为小写yyyy毫秒为SSS
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
// ================================== 工具方法(静态方法,无状态设计)=================================
/**
* 获取当前时间的格式化字符串
* 格式yyyyMMdd_HHmmssSSS年-月-日_时-分-秒-毫秒)
* @return 格式化后的当前时间字符串
*/
public static String getDateNowString() {
// 日期类转化成字符串类的工具
SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("YYYYMMdd_HHmmssmmm", java.util.Locale.getDefault());
// 读取当前时间
long nTimeNow = System.currentTimeMillis();
return mSimpleDateFormat.format(nTimeNow);
LogUtils.d(TAG, "【getDateNowString】调用开始");
// 初始化日期格式化工具Java 7 兼容使用小写yyyy避免周基年问题
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_PATTERN, DEFAULT_LOCALE);
// 读取当前时间戳
long currentTime = System.currentTimeMillis();
// 格式化时间
String formattedTime = sdf.format(currentTime);
LogUtils.d(TAG, "【getDateNowString】调用成功 | 格式化时间=" + formattedTime);
return formattedTime;
}
}

View File

@@ -3,12 +3,10 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -17,85 +15,111 @@ import java.io.IOException;
* 适配 PowerBell 项目支持指定保存路径、自动创建目录、处理PNG图片压缩
*/
public class DrawableToFileUtils {
private static final String TAG = "DrawableToFileUtils";
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "DrawableToFileUtils";
private static final String IMAGE_FORMAT_PNG = ".png"; // 目标图片格式
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.PNG; // 压缩格式
private static final int COMPRESS_QUALITY = 100; // PNG无损压缩质量
private static final long MIN_FILE_SIZE = 100; // 有效文件最小字节数
// ================================== 核心工具方法(基础版:指定文件路径)=================================
/**
* 核心方法:将 R.drawable 图片保存为 File 对象
* @param context 上下文(用于获取 Resources
* @param drawableResId 图片资源ID如 R.drawable.ic_test_png
* @param fileName 保存的文件名(需带 .png 后缀,如 "test_drawable.png"
* @param filePath 保存的文件路径(可带/不带.png后缀
* @return 保存成功返回 File 对象,失败返回 null
*/
public static File saveDrawableToFile(Context context, int drawableResId, String filePath) {
// 1. 校验参数(避免空指针/无效参数)
if (context == null || drawableResId == 0 || filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "【保存失败】参数无效context为空/资源ID为0/文件名为空)");
LogUtils.d(TAG, "【saveDrawableToFile】调用开始 | 资源ID=" + drawableResId + " | 目标路径=" + filePath);
// 1. 校验核心参数(避免空指针/无效参数)
if (context == null) {
LogUtils.e(TAG, "【saveDrawableToFile】参数异常context为空");
return null;
}
if (!filePath.endsWith(".png")) {
filePath += ".png"; // 强制添加 .png 后缀,确保图片格式正确
LogUtils.d(TAG, "【格式适配】自动添加.png后缀最终文件名" + filePath);
if (drawableResId == 0) {
LogUtils.e(TAG, "【saveDrawableToFile】参数异常drawableResId为0");
return null;
}
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "【saveDrawableToFile】参数异常filePath为空");
return null;
}
// 3. 构建目标 File 对象(最终保存的文件路径
File targetFile = new File(filePath);
LogUtils.d(TAG, "【保存路径】目标文件路径:" + targetFile.getAbsolutePath());
// 2. 格式化文件路径(强制添加.png后缀
String targetFilePath = filePath.endsWith(IMAGE_FORMAT_PNG) ? filePath : filePath + IMAGE_FORMAT_PNG;
if (!filePath.equals(targetFilePath)) {
LogUtils.d(TAG, "【saveDrawableToFile】格式适配自动添加.png后缀 | 最终路径=" + targetFilePath);
}
// 4. 读取 drawable 资源为 Bitmap处理高清图/缩放问题)
Bitmap bitmap = null;
try {
// 读取 drawable 资源(适配不同分辨率的图片,避免变形)
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
if (bitmap == null) {
LogUtils.e(TAG, "读取失败】无法读取drawable资源资源ID" + drawableResId + "");
// 3. 构建目标File对象并创建父目录
File targetFile = new File(targetFilePath);
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
boolean isDirCreated = parentDir.mkdirs();
if (!isDirCreated) {
LogUtils.e(TAG, "saveDrawableToFile】目录创建失败" + parentDir.getAbsolutePath());
return null;
}
LogUtils.d(TAG, "读取成功】drawable资源转Bitmap成功" + bitmap.getWidth() + ",高:" + bitmap.getHeight() + "");
LogUtils.d(TAG, "saveDrawableToFile】目录创建成功" + parentDir.getAbsolutePath());
}
LogUtils.d(TAG, "【saveDrawableToFile】目标文件路径" + targetFile.getAbsolutePath());
// 5. 将 Bitmap 写入 FilePNG格式无损保存
// 4. 读取drawable资源为Bitmap
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeResource(context.getResources(), drawableResId);
if (bitmap == null) {
LogUtils.e(TAG, "【saveDrawableToFile】读取失败无法解析drawable资源资源ID=" + drawableResId + "");
return null;
}
LogUtils.d(TAG, "【saveDrawableToFile】读取成功Bitmap尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight());
// 5. 将Bitmap写入FilePNG无损保存
FileOutputStream fos = new FileOutputStream(targetFile);
// 压缩参数PNG格式质量100无损写入输出流
boolean isSaved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush(); // 刷新输出流
fos.close(); // 关闭输出流
boolean isSaved = bitmap.compress(COMPRESS_FORMAT, COMPRESS_QUALITY, fos);
fos.flush();
fos.close();
// 6. 校验保存结果(文件是否存在且有效)
if (isSaved && targetFile.exists() && targetFile.length() > 100) {
LogUtils.d(TAG, "保存成功】drawable图片保存为File" + targetFile.getAbsolutePath());
return targetFile; // 保存成功返回File对象
// 6. 校验保存结果
if (isSaved && targetFile.exists() && targetFile.length() > MIN_FILE_SIZE) {
LogUtils.d(TAG, "saveDrawableToFile】保存成功" + targetFile.getAbsolutePath());
return targetFile;
} else {
LogUtils.e(TAG, "保存失败】图片写入文件无效(文件大小:" + (targetFile.exists() ? targetFile.length() : 0) + "字节)");
// 保存失败,删除无效文件
LogUtils.e(TAG, "saveDrawableToFile】保存失败文件无效存在=" + targetFile.exists() + " | 大小=" + targetFile.length() + "字节)");
// 清理无效文件
if (targetFile.exists()) {
targetFile.delete();
LogUtils.d(TAG, "【清理无效文件】已删除无效文件:" + targetFile.getAbsolutePath());
LogUtils.d(TAG, "saveDrawableToFile】清理无效文件:" + targetFile.getAbsolutePath());
}
return null;
}
} catch (IOException e) {
LogUtils.e(TAG, "【保存异常】写入文件时出错" + e.getMessage());
LogUtils.e(TAG, "【异常堆栈】" + Log.getStackTraceString(e));
LogUtils.e(TAG, "saveDrawableToFile】保存异常:" + e.getMessage());
return null;
} finally {
// 回收Bitmap资源避免内存溢出
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
LogUtils.d(TAG, "【资源回收Bitmap资源已回收");
LogUtils.d(TAG, "saveDrawableToFile】资源回收Bitmap已回收");
}
}
}
// ================================== 重载工具方法(扩展版:指定目录+文件名)=================================
/**
* 重载方法:自定义保存路径(灵活适配不同场景)
* @param context 上下文
* @param drawableResId 图片资源ID
* @param saveDirPath 自定义保存目录路径(如 "/storage/emulated/0/PowerBell/custom/"
* @param fileName 保存的文件名(带.png后缀
* @param fileName 保存的文件名(可带/不带.png后缀
* @return 保存成功返回File对象失败返回null
*/
public static File saveDrawableToFile(Context context, int drawableResId, String saveDirPath, String fileName) {
File filePath = new File(saveDirPath, fileName);
return saveDrawableToFile(context, drawableResId, filePath.getAbsolutePath());
LogUtils.d(TAG, "【saveDrawableToFile】重载方法调用开始 | 资源ID=" + drawableResId + " | 目录=" + saveDirPath + " | 文件名=" + fileName);
// 构建完整文件路径
File targetFile = new File(saveDirPath, fileName);
return saveDrawableToFile(context, drawableResId, targetFile.getAbsolutePath());
}
}

View File

@@ -4,33 +4,37 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.*;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import android.content.Context;
import android.net.Uri;
/**
* 文件操作工具类
* 功能:文件读写、复制、图片转换、文件名处理等常用文件操作
* 适配Java 7+,支持Android全版本
* 适配Java 7 + Android API 30
* 注意调用文件操作前需确保已获取存储权限Android 6.0+ 需动态申请)
*/
public class FileUtils {
/** 日志标签 */
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "FileUtils";
/** 读取文件默认缓冲区大小10KB */
private static final int BUFFER_SIZE = 10240;
/** 最大读取文件大小1GB防止OOM */
private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024;
/** 最大文件后缀长度(避免异常文件名) */
private static final int MAX_SUFFIX_LENGTH = 5;
/** 缓冲区大小(流复制专用) */
private static final int STREAM_BUFFER_SIZE = 1024;
// ====================================== 文件读取相关 ======================================
// ================================== 文件读取相关(字符串 + 字节数组)=================================
/**
* 读取文件内容并转为字符串
* @param filePath 文件绝对路径(非空)
@@ -38,25 +42,34 @@ public class FileUtils {
* @throws IOException 异常:文件不存在、文件过大、读取失败等
*/
public static String readFileAsString(String filePath) throws IOException {
LogUtils.d(TAG, "【readFileAsString】调用开始 | 文件路径=" + filePath);
// 1. 校验文件合法性
File file = new File(filePath);
if (!file.exists()) {
LogUtils.e(TAG, "【readFileAsString】文件不存在" + filePath);
throw new FileNotFoundException("文件不存在:" + filePath);
}
if (file.length() > MAX_READ_FILE_SIZE) {
LogUtils.e(TAG, "【readFileAsString】文件过大超过1GB" + filePath);
throw new IOException("文件过大超过1GB禁止读取" + filePath);
}
// 2. 读取文件内容使用StringBuilder高效拼接
StringBuilder sb = new StringBuilder((int) file.length());
try (FileInputStream fis = new FileInputStream(file)) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
// 循环读取缓冲区避免一次性读取大文件导致OOM
while ((readLen = fis.read(buffer)) > 0) {
sb.append(new String(buffer, 0, readLen));
}
} finally {
if (fis != null) {
fis.close();
}
}
LogUtils.d(TAG, "【readFileAsString】读取成功 | 文件大小=" + file.length() + "字节");
return sb.toString();
}
@@ -67,28 +80,39 @@ public class FileUtils {
* @throws IOException 异常:文件不存在、读取失败等
*/
public static byte[] readFileByBytes(String filePath) throws IOException {
LogUtils.d(TAG, "【readFileByBytes】调用开始 | 文件路径=" + filePath);
// 1. 校验文件合法性
File file = new File(filePath);
if (!file.exists()) {
LogUtils.e(TAG, "【readFileByBytes】文件不存在" + filePath);
throw new FileNotFoundException("文件不存在:" + filePath);
}
// 2. 缓冲流读取高效减少IO次数
try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
ByteArrayOutputStream bos = null;
BufferedInputStream bis = null;
try {
bos = new ByteArrayOutputStream((int) file.length());
bis = new BufferedInputStream(new FileInputStream(file));
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
while ((readLen = bis.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
}
bos.flush();
LogUtils.d(TAG, "【readFileByBytes】读取成功 | 文件大小=" + file.length() + "字节");
return bos.toByteArray();
} finally {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
}
}
// ====================================== 文件复制相关 ======================================
// ================================== 文件复制相关FileChannel + 简化版 + 流复制)=================================
/**
* 基于FileChannel复制文件高效适用于大文件复制
* @param source 源文件(非空,必须存在)
@@ -96,62 +120,117 @@ public class FileUtils {
* @throws IOException 异常:源文件不存在、复制失败等
*/
public static void copyFileUsingFileChannels(File source, File dest) throws IOException {
LogUtils.d(TAG, "【copyFileUsingFileChannels】调用开始 | 源文件=" + source.getAbsolutePath() + " | 目标文件=" + dest.getAbsolutePath());
// 1. 校验源文件合法性
if (!source.exists() || !source.isFile()) {
LogUtils.e(TAG, "【copyFileUsingFileChannels】源文件无效" + source.getAbsolutePath());
throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath());
}
// 2. 创建目标文件父目录
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
LogUtils.d(TAG, "【copyFileUsingFileChannels】创建父目录" + dest.getParentFile().getAbsolutePath());
}
// 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭)
try (FileChannel inputChannel = new FileInputStream(source).getChannel();
FileChannel outputChannel = new FileOutputStream(dest).getChannel()) {
// 从输入通道复制到输出通道(高效,底层优化)
// 3. 通道复制(手动关闭兼容Java 7
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
LogUtils.d(TAG, "文件复制成功FileChannel" + source.getAbsolutePath() + "" + dest.getAbsolutePath());
LogUtils.d(TAG, "【copyFileUsingFileChannels】复制成功");
} finally {
if (inputChannel != null) {
inputChannel.close();
}
if (outputChannel != null) {
outputChannel.close();
}
}
}
/**
* 简化版文件复制(基于NIO Files工具类代码简洁,适用于中小文件)
* 简化版文件复制(基于传统IO兼容全版本,适用于中小文件)
* @param oldFile 源文件(非空,必须存在)
* @param newFile 目标文件(非空,父目录会自动创建)
* @return 复制结果true-成功false-失败
*/
public static boolean copyFile(File oldFile, File newFile) {
LogUtils.d(TAG, "【copyFile】调用开始 | 源文件=" + (oldFile != null ? oldFile.getAbsolutePath() : "null") + " | 目标文件=" + (newFile != null ? newFile.getAbsolutePath() : "null"));
// 1. 校验源文件合法性
if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) {
LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null"));
LogUtils.e(TAG, "【copyFile】源文件无效");
return false;
}
// 2. 创建目标文件父目录
if (!newFile.getParentFile().exists()) {
newFile.getParentFile().mkdirs();
LogUtils.d(TAG, "【copyFile】创建父目录" + newFile.getParentFile().getAbsolutePath());
}
// 3. 复制文件(覆盖已有目标文件)
if (newFile.exists()) {
newFile.delete();
LogUtils.d(TAG, "【copyFile】删除已有目标文件" + newFile.getAbsolutePath());
}
try {
Path sourcePath = Paths.get(oldFile.getPath());
Path destPath = Paths.get(newFile.getPath());
// 先删除已有目标文件(避免覆盖失败)
if (newFile.exists()) {
newFile.delete();
}
Files.copy(sourcePath, destPath);
LogUtils.d(TAG, "文件复制成功Files" + oldFile.getAbsolutePath() + "" + newFile.getAbsolutePath());
copyFileUsingFileChannels(oldFile, newFile);
return true;
} catch (Exception e) {
LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e);
LogUtils.e(TAG, "【copyFile】复制失败:" + e.getMessage(), e);
return false;
}
}
// ====================================== 图片文件相关 ======================================
/**
* 复制输入流到文件兼容Uri解析失败场景
* @param inputStream 输入流(非空)
* @param file 目标文件(非空)
* @throws IOException 异常:流关闭失败、目录创建失败等
*/
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
LogUtils.d(TAG, "【copyStreamToFile】调用开始 | 目标文件=" + file.getAbsolutePath());
// 1. 校验参数合法性
if (inputStream == null || file == null) {
LogUtils.e(TAG, "【copyStreamToFile】参数为空InputStream=" + (inputStream == null) + " | File=" + (file == null));
throw new IllegalArgumentException("InputStream或File不能为空");
}
// 2. 创建目标文件父目录
File parentDir = file.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
LogUtils.e(TAG, "【copyStreamToFile】无法创建父目录" + parentDir.getAbsolutePath());
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
}
// 3. 流复制手动关闭流兼容Java 7
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
byte[] buffer = new byte[STREAM_BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
LogUtils.d(TAG, "【copyStreamToFile】复制成功");
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【copyStreamToFile】关闭输入流失败" + e.getMessage());
}
if (outputStream != null) {
outputStream.close();
}
}
}
// ================================== 图片文件相关BitmapDrawable 获取)=================================
/**
* 从文件路径获取BitmapDrawable适用于Android图片显示
* @param path 图片文件绝对路径(非空)
@@ -159,40 +238,50 @@ public class FileUtils {
* @throws IOException 异常文件读取IO错误
*/
public static BitmapDrawable getImageDrawable(String path) throws IOException {
LogUtils.d(TAG, "【getImageDrawable】调用开始 | 图片路径=" + path);
// 1. 校验文件合法性
File file = new File(path);
if (!file.exists() || !file.isFile()) {
LogUtils.e(TAG, "图片文件不存在" + path);
LogUtils.e(TAG, "【getImageDrawable】图片文件无效" + path);
return null;
}
// 2. 读取文件并转为BitmapDrawable缓冲流读取减少内存占用
try (InputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
is = new FileInputStream(file);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int readLen;
while ((readLen = is.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
}
// 3. 生成Bitmap并包装为BitmapDrawable
byte[] imageBytes = bos.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
LogUtils.d(TAG, "【getImageDrawable】转换成功 | 图片尺寸=" + bitmap.getWidth() + "x" + bitmap.getHeight());
return new BitmapDrawable(bitmap);
} finally {
if (is != null) {
is.close();
}
if (bos != null) {
bos.close();
}
}
}
// ====================================== 文件名处理相关 ======================================
// ================================== 文件名处理相关(后缀截取 + 唯一文件名)=================================
/**
* 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png"
* @param file 目标文件可为null
* @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串
*/
public static String getFileSuffixWithMultiDot(File file) {
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null"));
// 1. 校验文件合法性
if (file == null || !file.isFile()) {
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】文件无效返回空后缀");
return "";
}
@@ -201,14 +290,39 @@ public class FileUtils {
int lastDotIndex = fileName.lastIndexOf(".");
// 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件)
if (lastDotIndex == -1 // 无 "."
|| lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore"
|| (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5异常文件名
if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1 || (fileName.length() - lastDotIndex) > MAX_SUFFIX_LENGTH) {
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】无有效后缀 | 文件名=" + fileName);
return "";
}
// 4. 返回小写后缀(统一格式,避免大小写不一致问题)
return fileName.substring(lastDotIndex).toLowerCase();
String suffix = fileName.substring(lastDotIndex).toLowerCase();
LogUtils.d(TAG, "【getFileSuffixWithMultiDot】获取成功 | 后缀=" + suffix);
return suffix;
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
LogUtils.d(TAG, "【getFileSuffix】调用开始 | 文件=" + (file != null ? file.getAbsolutePath() : "null"));
if (file == null || file.getName().isEmpty()) {
LogUtils.d(TAG, "【getFileSuffix】文件无效返回空后缀");
return "";
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
LogUtils.d(TAG, "【getFileSuffix】无有效后缀 | 文件名=" + fileName);
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
String suffix = fileName.substring(lastDotIndex + 1).toLowerCase();
LogUtils.d(TAG, "【getFileSuffix】获取成功 | 后缀=" + suffix);
return suffix;
}
/**
@@ -218,73 +332,35 @@ public class FileUtils {
* @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点)
*/
public static String createUniqueFileName(File refFile) {
LogUtils.d(TAG, "【createUniqueFileName】调用开始 | 参考文件=" + (refFile != null ? refFile.getAbsolutePath() : "null"));
// 1. 获取参考文件的后缀名自动容错null/无效文件)
String suffix = getFileSuffixWithMultiDot(refFile);
// 2. 生成唯一标识UUID确保全局唯一时间戳进一步降低重复概率
String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名
String uniqueId = UUID.randomUUID().toString().replace("-", "");
long timeStamp = System.currentTimeMillis();
// 3. 拼接文件名(分场景处理,避免多余点)
String fileName;
if (suffix.isEmpty()) {
// 无后缀唯一ID + 时间戳
return String.format("%s_%d", uniqueId, timeStamp);
fileName = String.format("%s_%d", uniqueId, timeStamp);
} else {
// 有后缀唯一ID + 时间戳 + 后缀(无多余点)
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
fileName = String.format("%s_%d%s", uniqueId, timeStamp, suffix);
}
LogUtils.d(TAG, "【createUniqueFileName】生成成功 | 文件名=" + fileName);
return fileName;
}
/**
* 复制输入流到文件兼容Uri解析失败场景
*/
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
if (inputStream == null || file == null) {
throw new IllegalArgumentException("InputStream或File不能为空");
}
File parentDir = file.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
}
try {
OutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage());
}
}
}
public static boolean isFileExists(String path) {
File file = new File(path);
return file.exists();
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
if (file == null || file.getName().isEmpty()) {
return ""; // 空文件/空文件名,返回空
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
// ================================== 工具辅助方法(文件存在性判断)=================================
/**
* 判断文件是否存在
* @param path 文件绝对路径
* @return true-存在false-不存在
*/
public static boolean isFileExists(String path) {
LogUtils.d(TAG, "【isFileExists】调用开始 | 文件路径=" + path);
File file = new File(path);
boolean exists = file.exists();
LogUtils.d(TAG, "【isFileExists】判断结果 | 路径=" + path + " | 存在=" + exists);
return exists;
}
}

View File

@@ -2,27 +2,35 @@ package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import java.io.File;
import java.util.regex.Pattern;
/**
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File传参)
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File/BackgroundBean 多传参)
* 适配Java 7 + Android API 30
* 核心策略:强制 PNG 输出,保留透明通道,统一裁剪配置
*/
public class ImageCropUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 强制输出格式:固定为 PNG保留透明通道
private static final String FORCE_OUTPUT_SUFFIX = "png";
private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG;
private static final Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;
// 图片后缀正则(用于强制替换)
private static final Pattern IMAGE_SUFFIX_PATTERN = Pattern.compile("\\.(jpg|jpeg|png|bmp|gif)$", Pattern.CASE_INSENSITIVE);
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)======================
// ================================== 核心裁剪方法重载Uri/File/BackgroundBean=================================
/**
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
@@ -40,18 +48,19 @@ public class ImageCropUtils {
int aspectY,
boolean isFreeCrop,
int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始Uri 版)| 请求码=" + requestCode);
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "裁剪异常Activity 无效或已销毁");
LogUtils.e(TAG, "startImageCrop】参数异常Activity 无效或已销毁");
return;
}
if (inputUri == null || outputUri == null) {
LogUtils.e(TAG, "裁剪异常输入/输出 Uri 为空");
LogUtils.e(TAG, "startImageCrop】参数异常输入/输出 Uri 为空");
showToast(activity, "图片 Uri 无效,无法裁剪");
return;
}
if (!isValidUri(activity, inputUri)) {
LogUtils.e(TAG, "裁剪异常输入 Uri 无效" + inputUri);
LogUtils.e(TAG, "startImageCrop】参数异常输入 Uri 无效 " + inputUri);
showToast(activity, "原图 Uri 无效,无法裁剪");
return;
}
@@ -59,22 +68,23 @@ public class ImageCropUtils {
// 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG
File outputFile = uriToFile(activity, outputUri);
if (outputFile == null) {
LogUtils.e(TAG, "裁剪异常输出 Uri 转 File 失败" + outputUri);
LogUtils.e(TAG, "startImageCrop】转换异常输出 Uri 转 File 失败 " + outputUri);
showToast(activity, "裁剪输出路径无效");
return;
}
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
LogUtils.d(TAG, "【startImageCrop】格式修正强制输出 PNG " + outputFile.getAbsolutePath());
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "裁剪启动成功Uri 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
LogUtils.d(TAG, "startImageCrop】启动成功Uri 版)| 输出路径=" + outputFile.getAbsolutePath());
}
/**
@@ -94,18 +104,19 @@ public class ImageCropUtils {
int aspectY,
boolean isFreeCrop,
int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始File 版)| 请求码=" + requestCode);
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "裁剪异常Activity 无效或已销毁");
LogUtils.e(TAG, "startImageCrop】参数异常Activity 无效或已销毁");
return;
}
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "裁剪异常输入图片文件无效");
LogUtils.e(TAG, "startImageCrop】参数异常输入图片文件无效 " + (inputFile != null ? inputFile.getAbsolutePath() : "null"));
showToast(activity, "无有效图片可裁剪");
return;
}
if (outputFile == null) {
LogUtils.e(TAG, "裁剪异常输出文件路径为空");
LogUtils.e(TAG, "startImageCrop】参数异常输出文件路径为空");
showToast(activity, "裁剪输出路径无效");
return;
}
@@ -114,20 +125,27 @@ public class ImageCropUtils {
Uri inputUri = getFileProviderUri(activity, inputFile);
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
Uri outputUri = getFileProviderUri(activity, outputFile);
LogUtils.d(TAG, "【startImageCrop】格式修正强制输出 PNG " + outputFile.getAbsolutePath());
// 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY);
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
LogUtils.d(TAG, "startImageCrop】启动成功File 版)| 输出路径=" + outputFile.getAbsolutePath());
}
/**
* 【BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param cropBean 背景图片 Bean
* @param aspectX 固定比例 X
* @param aspectY 固定比例 Y
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
BackgroundBean cropBean,
@@ -135,166 +153,231 @@ public class ImageCropUtils {
int aspectY,
boolean isFreeCrop,
int requestCode) {
LogUtils.d(TAG, "【startImageCrop】调用开始BackgroundBean 版)| 请求码=" + requestCode);
if (cropBean == null) {
LogUtils.e(TAG, "【startImageCrop】参数异常BackgroundBean 为空");
showToast(activity, "裁剪参数无效");
return;
}
File inputFile = new File(cropBean.getBackgroundFilePath());
File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
LogUtils.d(TAG, "【startImageCrop】启动成功BackgroundBean 版)| 输入路径=" + inputFile.getAbsolutePath());
}
// ====================== 裁剪结果处理(保持兼容,优化日志)======================
// ================================== 裁剪结果处理(优化日志,增强容错)=================================
/**
* 处理裁剪结果
* @param requestCode 当前请求码
* @param resultCode 结果码
* @param data 结果数据
* @param cropRequestCode 裁剪请求码
* @return 裁剪成功返回输出路径,失败返回 null
*/
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
if (requestCode != cropRequestCode) return null;
LogUtils.d(TAG, "【handleCropResult】调用开始 | 请求码=" + requestCode + " | 裁剪请求码=" + cropRequestCode);
if (requestCode != cropRequestCode) {
LogUtils.d(TAG, "【handleCropResult】请求码不匹配忽略结果");
return null;
}
if (resultCode == Activity.RESULT_OK && data != null) {
Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) {
String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "裁剪成功】强制输出 PNG透明保留输出路径" + outputPath);
LogUtils.d(TAG, "handleCropResult】裁剪成功 | 输出路径=" + outputPath);
return outputPath;
} else {
LogUtils.e(TAG, "【handleCropResult】裁剪失败输出 Uri 为空");
}
} else if (resultCode == UCrop.RESULT_ERROR) {
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "裁剪失败】原因" + (error != null ? error.getMessage() : "未知错误"));
LogUtils.e(TAG, "handleCropResult】裁剪异常" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【裁剪取消用户手动取消");
LogUtils.d(TAG, "handleCropResult】裁剪取消用户手动取消");
}
return null;
}
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
/** 校验 Uri 有效性(确保是图片类型) */
// ================================== 私有辅助方法(参数校验 + 格式转换 + 配置初始化)=================================
/**
* 校验 Uri 有效性(确保是图片类型)
*/
private static boolean isValidUri(Activity activity, Uri uri) {
try {
String type = activity.getContentResolver().getType(uri);
return type != null && type.startsWith("image/");
boolean isValid = type != null && type.startsWith("image/");
LogUtils.d(TAG, "【isValidUri】Uri 校验结果 | " + uri + " | 有效=" + isValid);
return isValid;
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 校验失败】原因:" + e.getMessage());
LogUtils.e(TAG, "isValidUri】Uri 校验失败 " + uri, e);
return false;
}
}
/** Uri 转 File适配 FileProvider Uri 和普通 Uri */
/**
* Uri 转 File适配 FileProvider Uri 和普通 Uri
*/
private static File uriToFile(Activity activity, Uri uri) {
if (uri == null) return null;
if (uri == null) {
LogUtils.e(TAG, "【uriToFile】参数异常Uri 为空");
return null;
}
try {
if (uri.getScheme().equals("file")) {
return new File(uri.getPath());
File file = new File(uri.getPath());
LogUtils.d(TAG, "【uriToFile】转换成功普通 Uri| " + uri + "" + file.getAbsolutePath());
return file;
}
String filePath = uri.getPath();
if (filePath == null) return null;
if (filePath == null) {
LogUtils.e(TAG, "【uriToFile】转换失败Uri 路径为空 " + uri);
return null;
}
// 适配 FileProvider 路径
if (filePath.contains("/external_files/")) {
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
} else if (filePath.contains("/cache/")) {
filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/");
}
return new File(filePath);
File file = new File(filePath);
LogUtils.d(TAG, "【uriToFile】转换成功FileProvider Uri| " + uri + "" + file.getAbsolutePath());
return file;
} catch (Exception e) {
LogUtils.e(TAG, "UriFile 失败】uri=" + uri + ",原因:" + e.getMessage());
LogUtils.e(TAG, "uriToFile】转换失败 " + uri, e);
return null;
}
}
/** Uri 提取文件路径 */
/**
* Uri 提取文件路径
*/
private static String uriToPath(Uri uri) {
if (uri == null) return null;
if (uri == null) {
LogUtils.e(TAG, "【uriToPath】参数异常Uri 为空");
return null;
}
try {
if (uri.getScheme().equals("file")) {
return uri.getPath();
String path = uri.getPath();
LogUtils.d(TAG, "【uriToPath】提取成功普通 Uri| " + uri + "" + path);
return path;
}
String path = uri.getPath();
if (path == null) return null;
if (path == null) {
LogUtils.e(TAG, "【uriToPath】提取失败Uri 路径为空 " + uri);
return null;
}
// 适配多种 FileProvider 前缀
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
for (String prefix : prefixes) {
if (path.contains(prefix)) {
path = path.substring(path.indexOf(prefix) + prefix.length());
String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
return externalRoot + "/" + path;
path = externalRoot + "/" + path;
LogUtils.d(TAG, "【uriToPath】提取成功FileProvider Uri| " + uri + "" + path);
return path;
}
}
LogUtils.d(TAG, "【uriToPath】提取成功默认路径| " + uri + "" + path);
return path;
} catch (Exception e) {
LogUtils.e(TAG, "Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
LogUtils.e(TAG, "uriToPath】提取失败 " + uri, e);
return null;
}
}
/**
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心)
* 移除 isPng 参数,全程用 PNG 配置
*/
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
LogUtils.d(TAG, "【initCropOptions】初始化裁剪配置 | 自由裁剪=" + isFreeCrop);
UCrop.Options options = new UCrop.Options();
// 裁剪模式配置(自由裁剪/固定比例)
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
options.setFreeStyleCropEnabled(isFreeCrop);
// 裁剪配置(优化体验
//options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
//options.setCompressionQuality(100); // 图片质量
//options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
//options.setToolbarTitle("图片裁剪"); // 工具栏标题
//options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
//options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
// 核心:强制 PNG 保留透明(固定配置,无需判断原图格式
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
// 3. 通用 UI 配置(保持原有风格)
// 通用 UI 配置(保持原有风格)
options.setHideBottomControls(true); // 隐藏底部控制栏
options.setToolbarTitle("图片裁剪");
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
LogUtils.d(TAG, "【initCropOptions】配置完成强制 PNG 输出,保留透明通道");
return options;
}
/**
* 修正文件后缀(强制转为 .png,覆盖原有任何图片后缀)
* 修正文件后缀(强制转为指定后缀,覆盖原有任何图片后缀)
*/
private static File correctFileSuffix(File originFile, String targetSuffix) {
String originName = originFile.getName();
// 强制替换所有图片后缀为 targetSuffix(避免漏改)
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix;
return new File(originFile.getParent(), originName);
// 强制替换所有图片后缀为 targetSuffix
String newName = IMAGE_SUFFIX_PATTERN.matcher(originName).replaceAll("") + "." + targetSuffix;
File newFile = new File(originFile.getParent(), newName);
LogUtils.d(TAG, "【correctFileSuffix】后缀修正 | " + originFile.getName() + "" + newFile.getName());
return newFile;
}
/** 生成 FileProvider Uri适配 Android 7.0+ */
/**
* 生成 FileProvider Uri适配 Android 7.0+
*/
private static Uri getFileProviderUri(Activity activity, File file) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
return FileProvider.getUriForFile(activity, authority, file);
Uri uri = FileProvider.getUriForFile(activity, authority, file);
LogUtils.d(TAG, "【getFileProviderUri】生成成功Android 7.0+| " + file.getAbsolutePath() + "" + uri);
return uri;
} else {
return Uri.fromFile(file);
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【getFileProviderUri】生成成功Android 7.0-| " + file.getAbsolutePath() + "" + uri);
return uri;
}
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
LogUtils.e(TAG, "getFileProviderUri生成失败 " + file.getAbsolutePath(), e);
return null;
}
}
/** 显示 Toast避免崩溃 */
/**
* 显示 Toast避免崩溃
*/
private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "【showToast】显示提示" + msg);
} else {
LogUtils.e(TAG, "【showToast】无法显示提示Activity 无效");
}
}
// ====================== 公有辅助方法(供外部调用)======================
// ================================== 公有辅助方法(供外部调用)=================================
/**
* 公有方法:生成 FileProvider Uri
*/
public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file);
}
/**
* 公有方法Uri 转 File
*/
public static File getFileFromUriPublic(Activity activity, Uri uri) {
return uriToFile(activity, uri);
}
/**
* 公有方法Uri 提取路径
*/
public static String getPathFromUriPublic(Uri uri) {
return uriToPath(uri);
}

View File

@@ -1,9 +1,7 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import okhttp3.Call;
import okhttp3.Callback;
@@ -18,156 +16,167 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 图片下载工具类(单例模式)
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
* 适配Java 7 + Android API 30
* 核心策略OkHttp 全局复用、7天文件过期清理、UUID 唯一文件名、内置缓存目录(无需权限)
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 20:52
* @Describe 图片下载工具类(单例模式)
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
*/
public class ImageDownloader {
public static final String TAG = "ImageDownloader";
// 单例实例
private static ImageDownloader sInstance;
// OkHttp 客户端(全局复用,提升性能)
private OkHttpClient mOkHttpClient;
// 缓存目录:/data/data/应用包名/cache/networkdownload
private File mCacheDir;
// 过期时间7天单位毫秒可按需调整
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = "ImageDownloader";
// 缓存目录子文件夹名称
private static final String CACHE_DIR_NAME = "networkdownload";
// 过期时间7天单位毫秒
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
// OkHttp 超时配置
private static final int CONNECT_TIMEOUT = 10;
private static final int READ_WRITE_TIMEOUT = 15;
// 文件后缀最大长度
private static final int MAX_EXTENSION_LENGTH = 5;
// 默认文件后缀
private static final String DEFAULT_EXTENSION = ".jpg";
// 缓冲区大小
private static final int BUFFER_SIZE = 1024;
/**
* 私有构造(单例模式禁止外部实例化)
* @param context 上下文(用于获取缓存目录)
*/
private ImageDownloader(Context context) {
// 初始化 OkHttp 客户端(设置超时时间)
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
// ================================== 成员变量(单例核心 + 全局资源)=================================
// 单例实例
private static ImageDownloader sInstance;
// OkHttp 客户端(全局复用,提升性能)
private OkHttpClient mOkHttpClient;
// 缓存目录:/data/data/应用包名/cache/networkdownload
private File mCacheDir;
// ================================== 单例方法(线程安全 + 应用上下文)=================================
/**
* 单例获取方法(线程安全)
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
* @return 单例实例
*/
public static synchronized ImageDownloader getInstance(Context context) {
LogUtils.d(TAG, "【getInstance】单例获取方法调用");
if (sInstance == null) {
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏
sInstance = new ImageDownloader(context.getApplicationContext());
LogUtils.d(TAG, "【getInstance】单例实例首次创建");
}
return sInstance;
}
// ================================== 构造方法(私有 + 初始化逻辑)=================================
/**
* 私有构造(单例模式禁止外部实例化)
* @param context 应用上下文
*/
private ImageDownloader(Context context) {
LogUtils.d(TAG, "【ImageDownloader】构造方法调用开始初始化");
// 初始化 OkHttp 客户端(设置超时时间)
initOkHttpClient();
// 初始化缓存目录networkdownload
initCacheDir(context);
// 初始化时清理过期文件
clearExpiredFiles();
LogUtils.d(TAG, "【ImageDownloader】初始化完成");
}
// ================================== 核心初始化方法OkHttp + 缓存目录)=================================
/**
* 初始化 OkHttp 客户端(全局复用)
*/
private void initOkHttpClient() {
LogUtils.d(TAG, "【initOkHttpClient】开始初始化 OkHttp 客户端");
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(READ_WRITE_TIMEOUT, TimeUnit.SECONDS)
.build();
LogUtils.d(TAG, "【initOkHttpClient】OkHttp 客户端初始化完成");
}
// 初始化缓存目录networkdownload
initCacheDir(context);
// 初始化时清理过期文件
clearExpiredFiles();
}
/**
* 初始化缓存目录:若不存在则创建
* @param context 应用上下文
*/
private void initCacheDir(Context context) {
LogUtils.d(TAG, "【initCacheDir】开始初始化缓存目录");
// 获取应用内置缓存目录(无需权限)
File cacheRoot = context.getCacheDir();
mCacheDir = new File(cacheRoot, CACHE_DIR_NAME);
/**
* 单例获取方法(线程安全)
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
* @return 单例实例
*/
public static synchronized ImageDownloader getInstance(Context context) {
if (sInstance == null) {
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏
sInstance = new ImageDownloader(context.getApplicationContext());
}
return sInstance;
}
// 若目录不存在则创建(包括父目录)
if (!mCacheDir.exists()) {
boolean isCreated = mCacheDir.mkdirs();
if (isCreated) {
LogUtils.d(TAG, "【initCacheDir】缓存目录创建成功" + mCacheDir.getAbsolutePath());
} else {
LogUtils.e(TAG, "【initCacheDir】缓存目录创建失败");
}
} else {
LogUtils.d(TAG, "【initCacheDir】缓存目录已存在" + mCacheDir.getAbsolutePath());
}
}
/**
* 初始化缓存目录:若不存在则创建
*/
private void initCacheDir(Context context) {
// 获取应用内置缓存目录(无需权限)
File cacheRoot = context.getCacheDir();
mCacheDir = new File(cacheRoot, "networkdownload");
// ================================== 核心业务方法(下载 + 清理 + 获取最新文件)=================================
/**
* 下载网络图片到缓存目录
* @param imageUrl 图片网络链接
* @param callback 下载结果回调(成功/失败)
*/
public void downloadImage(final String imageUrl, final DownloadCallback callback) {
LogUtils.d(TAG, "【downloadImage】下载方法调用 | 图片链接=" + imageUrl);
// 1. 校验参数
if (TextUtils.isEmpty(imageUrl)) {
String errorMsg = "图片链接为空";
LogUtils.e(TAG, "【downloadImage】参数校验失败" + errorMsg);
if (callback != null) {
callback.onFailure(errorMsg);
}
return;
}
// 若目录不存在则创建(包括父目录)
if (!mCacheDir.exists()) {
boolean isCreated = mCacheDir.mkdirs();
if (isCreated) {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath());
} else {
LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败");
}
} else {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath());
}
}
if (mCacheDir == null || !mCacheDir.exists()) {
String errorMsg = "缓存目录不存在";
LogUtils.e(TAG, "【downloadImage】参数校验失败" + errorMsg);
if (callback != null) {
callback.onFailure(errorMsg);
}
return;
}
/**
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件)
*/
private void clearExpiredFiles() {
if (mCacheDir == null || !mCacheDir.exists()) {
return;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件,无需清理");
return;
}
long currentTime = System.currentTimeMillis();
int deleteCount = 0;
// 遍历所有文件,删除过期文件
for (File file : files) {
long lastModifyTime = file.lastModified();
if (currentTime - lastModifyTime > EXPIRE_TIME) {
if (file.delete()) {
deleteCount++;
LogUtils.d("ImageDownloader", "删除过期文件:" + file.getName());
} else {
LogUtils.e("ImageDownloader", "删除过期文件失败:" + file.getName());
}
}
}
LogUtils.d("ImageDownloader", "过期文件清理完成,共删除 " + deleteCount + " 个文件");
}
/**
* 下载网络图片到缓存目录
* @param imageUrl 图片网络链接
* @param callback 下载结果回调(成功/失败)
*/
public void downloadImage(final String imageUrl, final DownloadCallback callback) {
// 校验参数
if (TextUtils.isEmpty(imageUrl)) {
if (callback != null) {
callback.onFailure("图片链接为空");
}
return;
}
if (mCacheDir == null || !mCacheDir.exists()) {
if (callback != null) {
callback.onFailure("缓存目录不存在");
}
return;
}
// 构建 OkHttp 请求
Request request = new Request.Builder()
// 2. 构建 OkHttp 请求
Request request = new Request.Builder()
.url(imageUrl)
.build();
// 异步下载(避免阻塞主线程)
mOkHttpClient.newCall(request).enqueue(new Callback() {
// 3. 异步下载(避免阻塞主线程)
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下载失败,回调主线程
String errorMsg = "下载失败:" + e.getMessage();
LogUtils.e(TAG, "【downloadImage】OkHttp 下载失败", e);
if (callback != null) {
callback.onFailure("下载失败:" + e.getMessage());
callback.onFailure(errorMsg);
}
LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// 3.1 响应状态校验
if (!response.isSuccessful()) {
// 响应失败(如 404、500
String errorMsg = "响应失败:" + response.code();
LogUtils.e(TAG, "【downloadImage】响应失败状态码" + response.code());
if (callback != null) {
callback.onFailure("响应失败:" + response.code());
callback.onFailure(errorMsg);
}
// 关闭响应体
if (response.body() != null) {
response.body().close();
}
LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code());
return;
}
// 响应成功,写入文件
// 3.2 响应成功,写入文件
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
@@ -179,7 +188,7 @@ public class ImageDownloader {
// 写入文件
outputStream = new FileOutputStream(imageFile);
byte[] buffer = new byte[1024];
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
@@ -187,30 +196,32 @@ public class ImageDownloader {
outputStream.flush();
// 下载成功,回调主线程并返回文件路径
String filePath = imageFile.getAbsolutePath();
LogUtils.d(TAG, "【downloadImage】图片下载成功" + filePath);
if (callback != null) {
callback.onSuccess(imageFile.getAbsolutePath());
callback.onSuccess(filePath);
}
LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath());
} catch (IOException e) {
String errorMsg = "文件写入失败:" + e.getMessage();
LogUtils.e(TAG, "【downloadImage】文件写入失败", e);
if (callback != null) {
callback.onFailure("文件写入失败:" + e.getMessage());
callback.onFailure(errorMsg);
}
LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage());
} finally {
// 关闭流Java7 手动关闭,避免资源泄漏)
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
LogUtils.e(TAG, "【downloadImage】输入流关闭失败", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
LogUtils.e(TAG, "【downloadImage】输出流关闭失败", e);
}
}
// 关闭响应体
@@ -220,75 +231,119 @@ public class ImageDownloader {
}
}
});
}
}
/**
* 获取 networkdownload 目录中最后下载的文件(修改时间排序
* @return 最后下载的文件路径null 表示无文件)
*/
public String getLastDownloadedFile() {
if (mCacheDir == null || !mCacheDir.exists()) {
LogUtils.e("ImageDownloader", "缓存目录不存在");
return null;
}
/**
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件
*/
private void clearExpiredFiles() {
LogUtils.d(TAG, "【clearExpiredFiles】开始清理过期文件");
if (mCacheDir == null || !mCacheDir.exists()) {
LogUtils.d(TAG, "【clearExpiredFiles】缓存目录不存在,无需清理");
return;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件");
return null;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d(TAG, "【clearExpiredFiles】缓存目录无文件,无需清理");
return;
}
// 按最后修改时间降序排序,取第一个即为最新文件
File lastFile = files[0];
for (File file : files) {
if (file.lastModified() > lastFile.lastModified()) {
lastFile = file;
}
}
long currentTime = System.currentTimeMillis();
int deleteCount = 0;
LogUtils.d("ImageDownloader", "最后下载的文件:" + lastFile.getAbsolutePath());
return lastFile.getAbsolutePath();
}
// 遍历所有文件,删除过期文件
for (File file : files) {
long lastModifyTime = file.lastModified();
if (currentTime - lastModifyTime > EXPIRE_TIME) {
if (file.delete()) {
deleteCount++;
LogUtils.d(TAG, "【clearExpiredFiles】删除过期文件" + file.getName());
} else {
LogUtils.e(TAG, "【clearExpiredFiles】删除过期文件失败" + file.getName());
}
}
}
/**
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg
* @param imageUrl 图片链接
* @return 文件后缀(含点号,若无法提取则返回 .jpg
*/
private String getFileExtension(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return ".jpg";
}
LogUtils.d(TAG, "【clearExpiredFiles】过期文件清理完成共删除 " + deleteCount + " 个文件");
}
int lastDotIndex = imageUrl.lastIndexOf(".");
int lastSlashIndex = imageUrl.lastIndexOf("/");
// 确保后缀在最后一个斜杠之后且长度合理1-5 个字符)
if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) {
String extension = imageUrl.substring(lastDotIndex);
if (extension.length() <= 5) {
return extension.toLowerCase(); // 统一转为小写
}
}
/**
* 获取 networkdownload 目录中最后下载的文件(按修改时间排序)
* @return 最后下载的文件路径null 表示无文件)
*/
public String getLastDownloadedFile() {
LogUtils.d(TAG, "【getLastDownloadedFile】获取最新下载文件");
if (mCacheDir == null || !mCacheDir.exists()) {
LogUtils.e(TAG, "【getLastDownloadedFile】缓存目录不存在");
return null;
}
// 无法提取后缀时,默认使用 .jpg
return ".jpg";
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d(TAG, "【getLastDownloadedFile】缓存目录无文件");
return null;
}
/**
* 下载结果回调接口Java7 接口实现)
*/
public interface DownloadCallback {
/**
* 下载成功
* @param filePath 图片保存路径
*/
void onSuccess(String filePath);
// 按最后修改时间降序排序,取第一个即为最新文件
File lastFile = files[0];
for (File file : files) {
if (file.lastModified() > lastFile.lastModified()) {
lastFile = file;
}
}
/**
* 下载失败
* @param errorMsg 失败原因
*/
void onFailure(String errorMsg);
}
String filePath = lastFile.getAbsolutePath();
LogUtils.d(TAG, "【getLastDownloadedFile】最后下载的文件" + filePath);
return filePath;
}
// ================================== 辅助工具方法(文件后缀提取)=================================
/**
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg
* @param imageUrl 图片链接
* @return 文件后缀(含点号,若无法提取则返回 .jpg
*/
private String getFileExtension(String imageUrl) {
LogUtils.d(TAG, "【getFileExtension】提取文件后缀 | 图片链接=" + imageUrl);
if (TextUtils.isEmpty(imageUrl)) {
LogUtils.d(TAG, "【getFileExtension】图片链接为空返回默认后缀" + DEFAULT_EXTENSION);
return DEFAULT_EXTENSION;
}
int lastDotIndex = imageUrl.lastIndexOf(".");
int lastSlashIndex = imageUrl.lastIndexOf("/");
// 确保后缀在最后一个斜杠之后且长度合理1-5 个字符)
if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) {
String extension = imageUrl.substring(lastDotIndex);
if (extension.length() <= MAX_EXTENSION_LENGTH) {
extension = extension.toLowerCase(); // 统一转为小写
LogUtils.d(TAG, "【getFileExtension】提取后缀成功" + extension);
return extension;
}
}
// 无法提取后缀时,默认使用 .jpg
LogUtils.d(TAG, "【getFileExtension】无法提取有效后缀返回默认后缀" + DEFAULT_EXTENSION);
return DEFAULT_EXTENSION;
}
// ================================== 下载结果回调接口Java7 接口实现)=================================
/**
* 下载结果回调接口
*/
public interface DownloadCallback {
/**
* 下载成功
* @param filePath 图片保存路径
*/
void onSuccess(String filePath);
/**
* 下载失败
* @param errorMsg 失败原因
*/
void onFailure(String errorMsg);
}
}

View File

@@ -10,44 +10,100 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* 图片处理工具类(质量压缩专用)
* 功能对图片进行JPEG质量压缩并将压缩结果覆盖源文件
* 适配Java 7 + Android API 30
* 核心逻辑Bitmap.compress 质量压缩 + FileChannel 高效文件复制
*/
public class ImageUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = ImageUtils.class.getSimpleName();
private static final Bitmap.CompressFormat COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
private static final int MIN_COMPRESS_QUALITY = 0;
private static final int MAX_COMPRESS_QUALITY = 100;
// 这里我们生成了一个Pic文件夹在下面放了我们质量压缩后的图片用于和原图对比
// 压缩图片使用Bitmap.compress(),这里是质量压缩
// 参数Context context :调用本函数函数引用的资源体系
// String szSrcImagePath :要压缩的源文件路径
// String szDstImagePath :压缩后文件要保存的路径
// int nPictureCompress :图片压缩比例
public static void bitmapCompress(Context context, String szSrcImagePath, String szDstImagePath, int nPictureCompress) {
// ================================== 核心工具方法(图片质量压缩)=================================
/**
* 图片质量压缩JPEG格式压缩后覆盖源文件
* @param context 上下文(备用,当前逻辑未直接使用)
* @param srcImagePath 源图片文件路径(非空,文件需存在)
* @param dstImagePath 压缩后临时保存路径(非空,用于存储压缩中间文件)
* @param compressQuality 压缩质量0-100数值越小压缩率越高
*/
public static void bitmapCompress(Context context, String srcImagePath, String dstImagePath, int compressQuality) {
LogUtils.d(TAG, "【bitmapCompress】调用开始 | 源路径=" + srcImagePath + " | 临时路径=" + dstImagePath + " | 压缩质量=" + compressQuality);
// 1. 前置参数校验
if (srcImagePath == null || srcImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常源文件路径为空");
return;
}
if (dstImagePath == null || dstImagePath.isEmpty()) {
LogUtils.e(TAG, "【bitmapCompress】参数异常临时文件路径为空");
return;
}
if (compressQuality < MIN_COMPRESS_QUALITY || compressQuality > MAX_COMPRESS_QUALITY) {
LogUtils.e(TAG, "【bitmapCompress】参数异常压缩质量超出范围0-100当前值=" + compressQuality);
return;
}
File srcFile = new File(srcImagePath);
if (!srcFile.exists() || !srcFile.isFile()) {
LogUtils.e(TAG, "【bitmapCompress】源文件无效不存在或不是文件 " + srcImagePath);
return;
}
Bitmap compressBitmap = null;
OutputStream outputStream = null;
try {
Bitmap bmpCompressImage;
// 2. 读取源图片为Bitmap
compressBitmap = BitmapFactory.decodeFile(srcImagePath);
if (compressBitmap == null) {
LogUtils.e(TAG, "【bitmapCompress】Bitmap解码失败无法读取源图片 " + srcImagePath);
return;
}
LogUtils.d(TAG, "【bitmapCompress】Bitmap解码成功 | 尺寸=" + compressBitmap.getWidth() + "x" + compressBitmap.getHeight());
//生成新的文件
File fDstCompressImage = new File(szDstImagePath);
// 3. 创建临时压缩文件
File dstFile = new File(dstImagePath);
File dstParentDir = dstFile.getParentFile();
if (dstParentDir != null && !dstParentDir.exists()) {
boolean isDirCreated = dstParentDir.mkdirs();
LogUtils.d(TAG, "【bitmapCompress】临时目录创建" + (isDirCreated ? "成功" : "失败") + "" + dstParentDir.getAbsolutePath());
}
//裁剪后的图像转成BitMap
//photoBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uriClipUri));
bmpCompressImage = BitmapFactory.decodeFile(szSrcImagePath);
// 4. 写入压缩数据
outputStream = new FileOutputStream(dstFile);
boolean isCompressSuccess = compressBitmap.compress(COMPRESS_FORMAT, compressQuality, outputStream);
if (!isCompressSuccess) {
LogUtils.e(TAG, "【bitmapCompress】压缩失败Bitmap.compress 执行失败");
return;
}
LogUtils.d(TAG, "【bitmapCompress】压缩成功临时文件已生成 " + dstFile.getAbsolutePath());
//创建输出流
OutputStream out = null;
out = new FileOutputStream(fDstCompressImage.getPath());
//压缩文件,返回结果,参数分别是压缩的格式,压缩质量的百分比,输出流
boolean bCompress = bmpCompressImage.compress(Bitmap.CompressFormat.JPEG, nPictureCompress, out);
// 复制压缩后的文件到源路径
File fSrcImage = new File(szSrcImagePath);
FileUtils.copyFileUsingFileChannels(fDstCompressImage, fSrcImage);
LogUtils.d(TAG, Integer.toString(nPictureCompress) + "%压缩结束。");
// 5. 复制压缩文件覆盖源文件
FileUtils.copyFileUsingFileChannels(dstFile, srcFile);
LogUtils.d(TAG, "【bitmapCompress】" + compressQuality + "%压缩结束:已覆盖源文件 " + srcImagePath);
} catch (FileNotFoundException e) {
LogUtils.d(TAG, "bitmapCompress FileNotFoundException : " + e.getMessage());
LogUtils.e(TAG, "bitmapCompress】文件未找到异常", e);
} catch (IOException e) {
LogUtils.d(TAG, "bitmapCompress IOException : " + e.getMessage());
LogUtils.e(TAG, "bitmapCompressIO异常", e);
} finally {
// 6. 资源释放:关闭输出流
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【bitmapCompress】输出流关闭失败", e);
}
}
// 7. 资源释放回收Bitmap
if (compressBitmap != null && !compressBitmap.isRecycled()) {
compressBitmap.recycle();
LogUtils.d(TAG, "【bitmapCompress】Bitmap资源已回收");
}
}
}
}

View File

@@ -1,33 +0,0 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.util.DisplayMetrics;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/14 11:14
* @Describe 米盟 MimoUtils
*/
public final class MimoUtils {
public static final String TAG = "Utils";
public static int dpToPx(Context context, float dp) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (dp * displayMetrics.density + 0.5f);
}
public static int pxToDp(Context context, float px) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (px / displayMetrics.density + 0.5f);
}
public static int pxToSp(Context context, float pxValue) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (pxValue / displayMetrics.scaledDensity + 0.5f);
}
public static int spToPx(Context context, float spValue) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return (int) (spValue * displayMetrics.scaledDensity + 0.5f);
}
}

View File

@@ -2,29 +2,69 @@ package cc.winboll.studio.powerbell.utils;
import android.app.ActivityManager;
import android.content.Context;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.List;
/**
* 服务状态工具类
* 功能:判断指定服务是否处于运行状态
* 适配Java 7 + Android API 30
* 注意Android 8.0+ 对后台服务限制严格,此方法仅适用于前台服务或兼容场景
*/
public class ServiceUtils {
// ================================== 静态常量区(置顶归类)=================================
public static final String TAG = ServiceUtils.class.getSimpleName();
// 最大查询服务数量
private static final int MAX_RUNNING_SERVICES = 1000;
public static boolean isServiceAlive(Context context, String szServiceName) {
// 获取Activity管理者对象
ActivityManager manager = (ActivityManager) context
.getSystemService(Context.ACTIVITY_SERVICE);
// 获取正在运行的服务此处设置最多取1000个
List<ActivityManager.RunningServiceInfo> runningServices = manager
.getRunningServices(1000);
if (runningServices.size() <= 0) {
// ================================== 核心工具方法(判断服务是否运行)=================================
/**
* 判断指定服务是否处于运行状态
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
* @param serviceName 服务完整类名com.example.app.service.DemoService
* @return true-服务运行中false-服务未运行或查询失败
*/
public static boolean isServiceAlive(Context context, String serviceName) {
LogUtils.d(TAG, "【isServiceAlive】调用开始 | 服务名称=" + serviceName);
// 1. 前置参数校验
if (context == null) {
LogUtils.e(TAG, "【isServiceAlive】参数异常Context 为空");
return false;
}
// 遍历若存在名字和传入的serviceName的一致则说明存在
for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) {
if (runningServiceInfo.service.getClassName().equals(szServiceName)) {
if (TextUtils.isEmpty(serviceName)) {
LogUtils.e(TAG, "【isServiceAlive】参数异常服务名称为空");
return false;
}
// 2. 获取 ActivityManager
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
LogUtils.e(TAG, "【isServiceAlive】获取 ActivityManager 失败");
return false;
}
// 3. 查询正在运行的服务
List<ActivityManager.RunningServiceInfo> runningServices = activityManager.getRunningServices(MAX_RUNNING_SERVICES);
if (runningServices == null || runningServices.size() <= 0) {
LogUtils.d(TAG, "【isServiceAlive】正在运行的服务列表为空");
return false;
}
// 4. 遍历服务列表,匹配目标服务
for (ActivityManager.RunningServiceInfo serviceInfo : runningServices) {
if (serviceInfo.service == null) {
continue;
}
String className = serviceInfo.service.getClassName();
if (serviceName.equals(className)) {
LogUtils.d(TAG, "【isServiceAlive】服务运行中 | 匹配成功:" + serviceName);
return true;
}
}
LogUtils.d(TAG, "【isServiceAlive】服务未运行 | 未匹配到:" + serviceName);
return false;
}
}

View File

@@ -1,114 +1,151 @@
package cc.winboll.studio.powerbell.utils;
import android.text.TextUtils;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList;
import java.util.Locale;
/**
* 字符串格式化工具类
* 功能:电量使用时间列表格式化、时间跨度计算
* 适配Java 7 + Android API 30
* 核心逻辑:将电池信息列表转换为指定格式字符串,计算时间戳之间的跨度并格式化
*/
public class StringUtils {
// ================================== 静态常量区(置顶归类,消除魔法值)=================================
public static final String TAG = StringUtils.class.getSimpleName();
// 时间跨度单位符号
private static final String UNIT_DAY = "";
private static final String UNIT_HOUR = "";
private static final String UNIT_MINUTE = "";
private static final String UNIT_SECOND_DEFAULT = "☆}";
// 时间计算常量
private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L;
private static final long MILLIS_PER_HOUR = 60 * 60 * 1000L;
private static final long MILLIS_PER_MINUTE = 60 * 1000L;
private static final long MILLIS_PER_SECOND = 1000L;
// 空字符串常量(替代 TextUtils.EMPTY保证 Java 7 兼容)
private static final String EMPTY_STRING = "";
// 电量改变使用分钟数列表
// List of power-changing usage minutes
//
public static String formatPCMListString(ArrayList<BatteryInfoBean> arrayListBatteryInfo) {
/* 调试数据
Time t1 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t1.set(4, 8, 0, 27, 4, 2022);
long ntime1 = t1.toMillis(true);
Time t2 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t2.set(9, 12, 3, 29, 4, 2022);
long ntime2 = t2.toMillis(true);
LogUtils.d(TAG, "ntime1 is " + Long.toString(ntime1));
LogUtils.d(TAG, "ntime2 is " + Long.toString(ntime2));
LogUtils.d(TAG, "getTimespanDifference(ntime1, ntime2) is " + getTimespanDifference(ntime1, ntime2));
*/
/*String sz = "";
for (int i = 0; i < lnTime.size() - 1; i++) {
sz += getTimespanDifference(lnTime.get(i), lnTime.get(i + 1));
}
return sz;*/
String sz = "";
for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) {
//LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue()));
sz = arrayListBatteryInfo.get(i).getBattetyValue() + "% " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz;
// ================================== 核心格式化方法(电量列表格式化)=================================
/**
* 格式化电量使用时间列表为单行字符串
* @param batteryInfoList 电池信息列表(非空)
* @return 格式化后的单行字符串,格式:"电量% 时间跨度 电量% 时间跨度 ..."
*/
public static String formatPCMListString(ArrayList<BatteryInfoBean> batteryInfoList) {
LogUtils.d(TAG, "【formatPCMListString】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null));
// 1. 参数校验
if (batteryInfoList == null || batteryInfoList.size() < 2) {
LogUtils.e(TAG, "【formatPCMListString】参数异常列表为空或长度不足2");
return EMPTY_STRING;
}
return sz;
}
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> arrayListBatteryInfo) {
String sz = "";
for (int i = 0; i < arrayListBatteryInfo.size() - 1; i++) {
//LogUtils.d(TAG, "arrayListBatteryInfo.get(i).getBattetyValue() is "+ Integer.toString(arrayListBatteryInfo.get(i).getBattetyValue()));
sz = "\n" + arrayListBatteryInfo.get(i).getBattetyValue() + "%\n " + getTimespanDifference(arrayListBatteryInfo.get(i).getTimeStamp(), arrayListBatteryInfo.get(i + 1).getTimeStamp()) + " " + sz;
String result = EMPTY_STRING;
// 2. 遍历列表,拼接字符串(倒序拼接)
for (int i = 0; i < batteryInfoList.size() - 1; i++) {
BatteryInfoBean currentBean = batteryInfoList.get(i);
BatteryInfoBean nextBean = batteryInfoList.get(i + 1);
// 空指针防护
if (currentBean == null || nextBean == null) {
LogUtils.w(TAG, "【formatPCMListString】列表项为空跳过当前索引" + i);
continue;
}
// 获取电量和时间跨度
int batteryValue = currentBean.getBattetyValue();
String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp());
// 倒序拼接
result = batteryValue + "% " + timeSpan + " " + result;
LogUtils.d(TAG, "【formatPCMListString】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan);
}
return sz;
LogUtils.d(TAG, "【formatPCMListString】格式化完成 | 结果长度=" + result.length());
return result;
}
// 获取时间之间的时间跨度字符串。
// Get timespan string between times.
// 返回值: {(几天/)(几小时/)(几分钟/)(几秒钟)}
// 返回值: {(几小时/)(几分钟/)(几秒钟)}
// 返回值: {(几分钟/)(几秒钟)}
// 返回值: {(几秒钟)}
// (注start == end 时) 返回值: {0}
/**
* 格式化电量使用时间列表为带换行的字符串
* @param batteryInfoList 电池信息列表(非空)
* @return 格式化后的带换行字符串,每行一个电量和时间跨度
*/
public static String formatPCMListStringWithEnter(ArrayList<BatteryInfoBean> batteryInfoList) {
LogUtils.d(TAG, "【formatPCMListStringWithEnter】调用开始 | 列表大小=" + (batteryInfoList != null ? batteryInfoList.size() : null));
// 1. 参数校验
if (batteryInfoList == null || batteryInfoList.size() < 2) {
LogUtils.e(TAG, "【formatPCMListStringWithEnter】参数异常列表为空或长度不足2");
return EMPTY_STRING;
}
String result = EMPTY_STRING;
// 2. 遍历列表,拼接字符串(倒序拼接,带换行)
for (int i = 0; i < batteryInfoList.size() - 1; i++) {
BatteryInfoBean currentBean = batteryInfoList.get(i);
BatteryInfoBean nextBean = batteryInfoList.get(i + 1);
// 空指针防护
if (currentBean == null || nextBean == null) {
LogUtils.w(TAG, "【formatPCMListStringWithEnter】列表项为空跳过当前索引" + i);
continue;
}
// 获取电量和时间跨度
int batteryValue = currentBean.getBattetyValue();
String timeSpan = getTimespanDifference(currentBean.getTimeStamp(), nextBean.getTimeStamp());
// 倒序拼接(带换行)
result = "\n" + batteryValue + "%\n " + timeSpan + " " + result;
LogUtils.d(TAG, "【formatPCMListStringWithEnter】循环拼接 | 索引=" + i + " | 电量=" + batteryValue + "% | 时间跨度=" + timeSpan);
}
LogUtils.d(TAG, "【formatPCMListStringWithEnter】格式化完成 | 结果长度=" + result.length());
return result;
}
// ================================== 时间跨度计算方法(核心工具方法)=================================
/**
* 计算两个时间戳之间的跨度并格式化为指定字符串
* @param start 开始时间戳(毫秒)
* @param end 结束时间戳(毫秒)
* @return 格式化的时间跨度字符串,格式:{天☀时★分✰秒} 或 {☆}当时间差为0时
*/
public static String getTimespanDifference(long start, long end) {
String szReturn = "{";
LogUtils.d(TAG, "【getTimespanDifference】调用开始 | 开始时间戳=" + start + " | 结束时间戳=" + end);
long between = end - start;
//LogUtils.d(TAG, "between is " + Long.toString(between));
long day = between / (24 * 60 * 60 * 1000);
long hour = (between / (60 * 60 * 1000) - day * 24);
long min = ((between / (60 * 1000)) - day * 24 * 60 - hour * 60);
long s = (between / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60);
/* 调试数据
day = 0;
hour = 2;
min = 0;
s = 7;
*/
LogUtils.d(TAG, "【getTimespanDifference】时间差毫秒=" + between);
//long ms = (between - day * 24 * 60 * 60 * 1000 - hour * 60 * 60 * 1000
//- min * 60 * 1000 - s * 1000);
// 计算天、时、分、秒
long day = between / MILLIS_PER_DAY;
long hour = (between % MILLIS_PER_DAY) / MILLIS_PER_HOUR;
long min = (between % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
long sec = (between % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
szReturn += day > 0 ? String.format(java.util.Locale.getDefault(), "%d☀", day) : "";
szReturn += hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d★", hour) : "";
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d✰", min) : "";
szReturn += min > 0 || hour > 0 || day > 0 ? String.format(java.util.Locale.getDefault(), "%d}", s) : "☆}";
// 拼接结果字符串
StringBuilder result = new StringBuilder("{");
boolean hasHigherUnit = false;
//String strmin = String.format("%02d", min);
//String strs = String.format("%02d", s);
//String strms = String.format("%03d",ms);
//String timeDifference = day + "天" + hour + "小时" + strmin + "分" + strs + "秒" + strms + "毫秒";
//String timeDifference = hour + getString(R.string.activity_main_msg_hour)
// + strmin + getString(R.string.activity_main_msg_minute)
// + strs + getString(R.string.activity_main_msg_second);
//return timeDifference;
// 拼接天
if (day > 0) {
result.append(String.format(Locale.getDefault(), "%d%s", day, UNIT_DAY));
hasHigherUnit = true;
}
// 拼接时(当天>0或后续有单位时
if (hour > 0 || hasHigherUnit) {
result.append(String.format(Locale.getDefault(), "%d%s", hour, UNIT_HOUR));
hasHigherUnit = true;
}
// 拼接分(当时>0或后续有单位时
if (min > 0 || hasHigherUnit) {
result.append(String.format(Locale.getDefault(), "%d%s", min, UNIT_MINUTE));
hasHigherUnit = true;
}
// 拼接秒或默认值
if (hasHigherUnit) {
result.append(String.format(Locale.getDefault(), "%d}", sec));
} else {
result.append(UNIT_SECOND_DEFAULT);
}
return szReturn;
String timeSpan = result.toString();
LogUtils.d(TAG, "【getTimespanDifference】计算完成 | 时间跨度=" + timeSpan);
return timeSpan;
}
// 调试函数: 调试formatPCMListString(ArrayList<Long> lnTime)
//
/*public static String formatPCMListString_test() {
// 调试数据
ArrayList<Long> listTime = new ArrayList<Long>();
Time t1 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t1.set(0, 8, 0, 27, 4, 2022);
long ntime1 = t1.toMillis(true);
listTime.add(ntime1);
for (int i = 0; i < 5; i++) {
Time t2 = new Time();
//t.set(int second, int minute, int hour, int monthDay, int month, int year) {}
t2.set(4, 8 + i + 1, 0, 27, 4, 2022);
long ntime2 = t2.toMillis(true);
listTime.add(ntime2);
}
return formatPCMListString(listTime);
//LogUtils.d(TAG, StringUtils.formatPCMListString(listTime));
}*/
}

View File

@@ -10,7 +10,6 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
@@ -25,67 +24,68 @@ import java.io.File;
* 改进:强制保持缓存策略,无论内存是否紧张,不自动清理任何缓存,保留图片原始品质
*/
public class BackgroundView extends RelativeLayout {
// ====================================== 静态常量区 ======================================
public static final String TAG = "BackgroundView";
// 记录当前已缓存的图片路径
private String mCurrentCachedPath = "";
// Bitmap 配置常量(原始品质)
private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int BITMAP_SAMPLE_SIZE = 1; // 不缩放采样率
// ====================================== 成员变量区 ======================================
// 缓存相关
private String mCurrentCachedPath = "";
// 视图相关
private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout
private ImageView mIvBackground; // 图片显示控件
// 图片属性
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
// ====================================== 构造器Java7兼容 ======================================
public BackgroundView(Context context) {
super(context);
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器1启动 [context] ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器2启动 [context, attrs] ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
LogUtils.d(TAG, "=== BackgroundView 构造器3启动 [context, attrs, defStyleAttr] ===");
this.mContext = context;
initView();
}
// ====================================== 初始化 ======================================
// ====================================== 初始化方法 ======================================
private void initView() {
LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
// 2. 初始化主容器LinearLayout
initLinearLayout();
// 3. 初始化ImageView
initImageView();
// 初始设置透明背景
// 4. 初始设置透明背景
setDefaultTransparentBackground();
LogUtils.d(TAG, "=== initView 完成 ===");
}
private void initLinearLayout() {
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL);
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
mLlContainer.setGravity(android.view.Gravity.CENTER);
mLlContainer.setBackgroundColor(0x00000000);
this.addView(mLlContainer);
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
@@ -94,106 +94,118 @@ public class BackgroundView extends RelativeLayout {
private void initImageView() {
LogUtils.d(TAG, "=== initImageView 启动 ===");
mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺(无拉伸裁剪)
mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ===");
}
// ====================================== 对外方法 ======================================
public void loadByBackgroundBean(BackgroundBean bean) {
loadByBackgroundBean(bean, false);
}
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
if (!bean.isUseBackgroundFile()) {
LogUtils.d(TAG, "=== loadByBackgroundBean 启动 [isRefresh:" + isRefresh + "] ===");
// 参数校验
if (bean == null) {
LogUtils.e(TAG, "loadByBackgroundBean: BackgroundBean为空");
setDefaultTransparentBackground();
return;
}
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
if (!(new File(targetPath).exists())) {
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
// 判断是否使用背景文件
if (!bean.isUseBackgroundFile()) {
LogUtils.d(TAG, "loadByBackgroundBean: 不使用背景文件,设置透明背景");
setDefaultTransparentBackground();
return;
}
// 核心修改:刷新时不删除旧缓存,仅重新解码原始品质图片并更新缓存(强制保持策略)
// 获取目标路径
String targetPath = bean.isUseBackgroundScaledCompressFile()
? bean.getBackgroundScaledCompressFilePath()
: bean.getBackgroundFilePath();
LogUtils.d(TAG, "loadByBackgroundBean: 目标路径=" + targetPath);
// 校验文件是否存在
File targetFile = new File(targetPath);
if (!targetFile.exists() || !targetFile.isFile()) {
LogUtils.e(TAG, "loadByBackgroundBean: 视图控件图片不存在:" + targetPath);
return;
}
// 刷新逻辑:重新解码原始品质图片并更新缓存
if (isRefresh) {
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存 - " + targetPath);
// 刷新时直接解码原始品质图片,更新缓存(不删除旧缓存)
Bitmap newBitmap = decodeOriginalBitmap(new File(targetPath));
LogUtils.d(TAG, "loadByBackgroundBean: 刷新图片,重新解码原始品质图片并更新缓存");
Bitmap newBitmap = decodeOriginalBitmap(targetFile);
if (newBitmap != null) {
App.sBitmapCacheUtils.cacheBitmap(targetPath, newBitmap);
// 增加引用计数
App.sBitmapCacheUtils.increaseRefCount(targetPath);
LogUtils.d(TAG, "loadByBackgroundBean: 刷新缓存成功,路径=" + targetPath);
} else {
LogUtils.e(TAG, "loadByBackgroundBean: 刷新解码失败,路径=" + targetPath);
}
}
// 加载图片
loadImage(targetPath);
LogUtils.d(TAG, "=== loadByBackgroundBean 完成 ===");
}
// ====================================== 对外方法 ======================================
/**
* 改进版:强制保持缓存策略,不自动清理任何缓存,强化引用计数管理,保留图片原始品质
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
// 1. 路径空校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "loadImage: 图片路径为空");
setDefaultTransparentBackground();
return;
}
// 2. 文件有效性校验
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile()) {
LogUtils.e(TAG, "图片文件无效");
LogUtils.e(TAG, "loadImage: 图片文件无效");
setDefaultTransparentBackground();
return;
}
// 3. 隐藏ImageView防止闪烁
mIvBackground.setVisibility(View.GONE);
// ======================== 路径判断逻辑(强制缓存版) ========================
// 1. 路径未变化:校验缓存有效性,无效则重加载(不删除旧缓存)
// 3.1 路径未变化:校验缓存有效性
if (imagePath.equals(mCurrentCachedPath)) {
Bitmap cachedBitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(cachedBitmap)) {
LogUtils.d(TAG, "loadImage: 路径未变,使用有效缓存 Bitmap原始品质");
LogUtils.d(TAG, "loadImage: 路径未变使用有效缓存Bitmap原始品质");
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
mIvBackground.setImageBitmap(cachedBitmap);
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成(缓存命中) ===");
return;
} else {
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载原始品质图片 - " + imagePath);
// 缓存无效,直接重加载原始品质图片(不删除旧缓存,强制保持策略)
LogUtils.e(TAG, "loadImage: 缓存Bitmap无效尝试重加载原始品质图片");
}
}
// 2. 路径已更新:保留旧缓存,仅更新当前路径记录(核心修改:不删除旧缓存)
// 3.2 路径已更新:保留旧缓存,仅更新路径记录
if (!TextUtils.isEmpty(mCurrentCachedPath) && !mCurrentCachedPath.equals(imagePath)) {
LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,当前路径从 " + mCurrentCachedPath + " 切换到 " + imagePath);
// 仅更新当前路径记录,不删除旧缓存
LogUtils.d(TAG, "loadImage: 路径已更新,保留旧缓存,原路径=" + mCurrentCachedPath + ",新路径=" + imagePath);
}
// ======================== 路径判断逻辑结束 ========================
// 无缓存/缓存无效/路径更新:重新加载原始品质图片
// 4. 计算图片宽高比
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
// 先尝试从缓存获取
// 5. 获取或解码Bitmap
Bitmap bitmap = App.sBitmapCacheUtils.getCachedBitmap(imagePath);
if (isBitmapValid(bitmap)) {
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap原始品质 - " + imagePath);
LogUtils.d(TAG, "loadImage: 从缓存获取有效Bitmap原始品质");
} else {
// 缓存无效应解码原始品质图片
LogUtils.d(TAG, "loadImage: 缓存未命中,解码原始品质图片");
bitmap = decodeOriginalBitmap(imageFile);
if (bitmap == null) {
LogUtils.e(TAG, "loadImage: 图片解码失败(原始品质)");
@@ -201,17 +213,15 @@ public class BackgroundView extends RelativeLayout {
setDefaultTransparentBackground();
return;
}
// 缓存新图片(强制保持,原始品质)
// 缓存新图片
App.sBitmapCacheUtils.cacheBitmap(imagePath, bitmap);
LogUtils.d(TAG, "loadImage: 加载新图片缓存(原始品质) - " + imagePath);
LogUtils.d(TAG, "loadImage: 新图片缓存成功,路径=" + imagePath);
}
// 增加引用计数(配合全局缓存工具的引用计数机制)
// 6. 引用计数管理
App.sBitmapCacheUtils.increaseRefCount(imagePath);
// 更新当前缓存路径记录
// 7. 更新当前缓存路径
mCurrentCachedPath = imagePath;
// 直接使用setImageBitmap避免BitmapDrawable包装的引用风险
// 8. 设置图片并调整尺寸
mIvBackground.setImageBitmap(bitmap);
adjustImageViewSize();
LogUtils.d(TAG, "=== loadImage 完成 ===");
@@ -222,27 +232,36 @@ public class BackgroundView extends RelativeLayout {
* 工具方法判断Bitmap是否有效非空且未被回收
*/
private boolean isBitmapValid(Bitmap bitmap) {
return bitmap != null && !bitmap.isRecycled();
boolean valid = bitmap != null && !bitmap.isRecycled();
if (!valid) {
LogUtils.w(TAG, "isBitmapValid: Bitmap无效空或已回收");
}
return valid;
}
/**
* 计算图片宽高比
*/
private boolean calculateImageAspectRatio(File file) {
LogUtils.d(TAG, "=== calculateImageAspectRatio 启动,文件=" + file.getAbsolutePath() + " ===");
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 尺寸校验
int width = options.outWidth;
int height = options.outHeight;
if (width <= 0 || height <= 0) {
LogUtils.e(TAG, "图片尺寸无效");
LogUtils.e(TAG, "calculateImageAspectRatio: 图片尺寸无效,宽=" + width + ",高=" + height);
return false;
}
// 计算比例
mImageAspectRatio = (float) width / height;
LogUtils.d(TAG, "原图比例" + mImageAspectRatio);
LogUtils.d(TAG, "calculateImageAspectRatio: 原图比例=" + mImageAspectRatio);
LogUtils.d(TAG, "=== calculateImageAspectRatio 完成 ===");
return true;
} catch (Exception e) {
LogUtils.e(TAG, "计算比例失败:" + e.getMessage());
LogUtils.e(TAG, "calculateImageAspectRatio: 计算比例失败:" + e.getMessage());
return false;
}
}
@@ -251,33 +270,46 @@ public class BackgroundView extends RelativeLayout {
* 移除压缩逻辑:解码原始品质图片(无缩放、无色彩损失)
*/
private Bitmap decodeOriginalBitmap(File file) {
LogUtils.d(TAG, "=== decodeOriginalBitmap 启动,文件=" + file.getAbsolutePath() + " ===");
try {
BitmapFactory.Options options = new BitmapFactory.Options();
// 核心配置:无缩放、全彩品质、关闭可回收标志(强制保持)
options.inSampleSize = 1; // 不缩放采样率为1
options.inPreferredConfig = Bitmap.Config.ARGB_8888; // 全彩品质,无色彩损失
options.inPurgeable = false; // 关闭可回收标志,防止系统主动回收
// 核心配置:原始品质
options.inSampleSize = BITMAP_SAMPLE_SIZE;
options.inPreferredConfig = BITMAP_CONFIG;
options.inPurgeable = false;
options.inInputShareable = false;
options.inDither = true; // 开启抖动,保证色彩还原精度
options.inScaled = false; // 关闭自动缩放,保留原始尺寸
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
options.inDither = true;
options.inScaled = false;
// 解码图片
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bitmap != null) {
LogUtils.d(TAG, "decodeOriginalBitmap: 解码成功,宽=" + bitmap.getWidth() + ",高=" + bitmap.getHeight());
} else {
LogUtils.e(TAG, "decodeOriginalBitmap: 解码返回null");
}
LogUtils.d(TAG, "=== decodeOriginalBitmap 完成 ===");
return bitmap;
} catch (Exception e) {
LogUtils.e(TAG, "原始品质解码失败:" + e.getMessage());
LogUtils.e(TAG, "decodeOriginalBitmap: 原始品质解码失败:" + e.getMessage());
return null;
}
}
/**
* 调整ImageView尺寸保持原图比例
*/
private void adjustImageViewSize() {
LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
// 空指针校验
if (mLlContainer == null || mIvBackground == null) {
LogUtils.e(TAG, "adjustImageViewSize: 容器或ImageView未初始化");
return;
}
// 获取容器尺寸
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth == 0 || llHeight == 0) {
LogUtils.w(TAG, "adjustImageViewSize: 容器尺寸未初始化,延迟调整");
// 延迟调整(容器尺寸未就绪时)
post(new Runnable() {
@Override
public void run() {
@@ -286,7 +318,7 @@ public class BackgroundView extends RelativeLayout {
});
return;
}
// 计算ImageView尺寸
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
@@ -295,28 +327,36 @@ public class BackgroundView extends RelativeLayout {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
// 设置尺寸
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 仅居中平铺,无缩放
mIvBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIvBackground.setVisibility(View.VISIBLE);
LogUtils.d(TAG, "adjustImageViewSize: 尺寸调整完成,宽=" + ivWidth + ",高=" + ivHeight);
LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
}
/**
* 设置默认透明背景,仅减少引用计数,不删除缓存
*/
private void setDefaultTransparentBackground() {
// 清空ImageView的Drawable释放本地引用不影响全局缓存
LogUtils.d(TAG, "=== setDefaultTransparentBackground 启动 ===");
// 清空ImageView
mIvBackground.setImageDrawable(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
// 核心修改:路径清空时减少引用计数,不删除缓存
// 减少引用计数,不删除缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, "setDefaultTransparentBackground: 减少引用计数,路径=" + mCurrentCachedPath);
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "=== setDefaultTransparentBackground 完成 ===");
}
// ====================================== 重写方法(核心改进) ======================================
// ====================================== 重写生命周期方法 ======================================
/**
* 重写绘制前强制校验Bitmap有效性防止已回收Bitmap崩溃
*/
@@ -341,14 +381,16 @@ public class BackgroundView extends RelativeLayout {
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,保留全局缓存");
LogUtils.d(TAG, "=== onDetachedFromWindow 启动 ===");
// 清空ImageView的Drawable释放本地引用
mIvBackground.setImageDrawable(null);
// 核心修改:仅减少引用计数,不删除全局缓存
// 减少引用计数,不删除全局缓存
if (!TextUtils.isEmpty(mCurrentCachedPath)) {
LogUtils.d(TAG, "onDetachedFromWindow: 减少引用计数,路径=" + mCurrentCachedPath);
App.sBitmapCacheUtils.decreaseRefCount(mCurrentCachedPath);
mCurrentCachedPath = "";
}
LogUtils.d(TAG, "=== onDetachedFromWindow 完成 ===");
}
/**
@@ -357,7 +399,8 @@ public class BackgroundView extends RelativeLayout {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
adjustImageViewSize(); // 恢复尺寸调整
LogUtils.d(TAG, "onSizeChanged: 尺寸变化,宽=" + w + ",高=" + h + ",调整ImageView尺寸");
adjustImageViewSize();
}
}

View File

@@ -9,32 +9,37 @@ import android.graphics.drawable.Drawable;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 12:55
* @Describe 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
*/
public class BatteryDrawable extends Drawable {
// ====================== 静态常量(置顶,按重要性排序) ======================
// ====================================== 静态常量区(按功能归类,消除魔法值) ======================================
public static final String TAG = "BatteryDrawable";
// 小米机型绘制偏移校准适配MIUI渲染特性避免绘制错位
private static final int MIUI_DRAW_OFFSET = 1;
// 默认电量透明度兼顾显示效果与API30渲染性能
private static final int DEFAULT_BATTERY_ALPHA = 210;
// 电量范围常量
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// 条纹风格拆分数量
private static final int STRIPE_COUNT = 100;
// ====================== 核心成员变量按功能归类final优先 ======================
// ====================================== 成员变量区final优先按功能归类 ======================================
// 绘制画笔final修饰避免重复创建提升性能
private final Paint mBatteryPaint;
// 业务控制变量
private int mBatteryValue = -1; // 当前电量0-100-1=未初始化)
private boolean mIsEnergyStyle = true; // 绘制风格true=能量false=条纹)
// ====================== 构造方法(重载适配,优先暴露常用构造) ======================
// ====================================== 构造方法(重载适配,优先暴露常用构造) ======================================
/**
* 构造方法(默认能量风格,常用场景)
* @param batteryColor 电量显示颜色
*/
public BatteryDrawable(int batteryColor) {
LogUtils.d(TAG, "constructor: 初始化(能量风格颜色=" + Integer.toHexString(batteryColor));
LogUtils.d(TAG, "【BatteryDrawable】构造器1调用 | 能量风格 | 颜色=" + Integer.toHexString(batteryColor));
mBatteryPaint = new Paint();
initPaintConfig(batteryColor);
}
@@ -45,85 +50,109 @@ public class BatteryDrawable extends Drawable {
* @param isEnergyStyle 是否启用能量风格
*/
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) {
LogUtils.d(TAG, "constructor: 初始化,颜色=" + Integer.toHexString(batteryColor) + "风格=" + (isEnergyStyle ? "能量" : "条纹"));
LogUtils.d(TAG, "【BatteryDrawable】构造器2调用 | 颜色=" + Integer.toHexString(batteryColor) + " | 风格=" + (isEnergyStyle ? "能量" : "条纹"));
mBatteryPaint = new Paint();
mIsEnergyStyle = isEnergyStyle;
initPaintConfig(batteryColor);
}
// ====================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================
// ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================================
/**
* 初始化画笔配置适配API30渲染特性优化小米机型兼容性
* @param color 电量显示颜色
*/
private void initPaintConfig(int color) {
LogUtils.d(TAG, "【initPaintConfig】画笔配置开始 | 颜色=" + Integer.toHexString(color));
mBatteryPaint.setColor(color);
mBatteryPaint.setAlpha(DEFAULT_BATTERY_ALPHA);
mBatteryPaint.setAntiAlias(true); // 抗锯齿,解决小米低分辨率锯齿问题
mBatteryPaint.setStyle(Paint.Style.FILL); // 固定填充模式,避免混乱
mBatteryPaint.setDither(false); // 禁用抖动提升API30颜色显示一致性
LogUtils.d(TAG, "initPaintConfig: 画笔配置完成");
LogUtils.d(TAG, "initPaintConfig画笔配置完成");
}
// ====================== 核心绘制方法Drawable抽象方法优先级最高 ======================
// ====================================== 核心绘制方法Drawable抽象方法优先级最高 ======================================
@Override
public void draw(Canvas canvas) {
LogUtils.d(TAG, "【draw】绘制开始 | 当前电量=" + mBatteryValue + " | 风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
// 未初始化/异常电量,直接跳过,避免无效绘制
if (mBatteryValue < 0) {
LogUtils.w(TAG, "draw: 电量未初始化,跳过绘制");
LogUtils.w(TAG, "draw电量未初始化,跳过绘制");
return;
}
// 强制校准电量范围0-100防止异常值导致绘制错误
int validBattery = Math.max(0, Math.min(mBatteryValue, 100));
int validBattery = Math.max(BATTERY_MIN, Math.min(mBatteryValue, BATTERY_MAX));
LogUtils.d(TAG, "【draw】电量校准完成 | 有效电量=" + validBattery);
Rect drawBounds = getBounds();
// 绘制边界空指针防护
if (drawBounds == null) {
LogUtils.e(TAG, "【draw】绘制边界为空跳过绘制");
return;
}
int drawHeight = drawBounds.height();
// 小米机型绘制偏移校准解决MIUI系统渲染偏移问题
int offset = MIUI_DRAW_OFFSET;
int left = drawBounds.left + offset;
int right = drawBounds.right - offset;
LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight);
// 按风格执行绘制(精简日志,仅保留核心绘制参数)
LogUtils.d(TAG, "draw: 开始绘制,电量=" + validBattery + ",风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
// 按风格执行绘制
if (mIsEnergyStyle) {
drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
} else {
drawStripeStyle(canvas, validBattery, left, right, drawHeight);
}
LogUtils.d(TAG, "【draw】绘制完成");
}
// ====================== 绘制风格实现(私有封装,按风格拆分) ======================
// ====================================== 绘制风格实现(私有封装,按风格拆分) ======================================
/**
* 能量风格绘制(整块填充,高效简洁,默认风格)
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/
private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) {
int top = height - (height * battery / 100); // 计算电量对应顶部坐标
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery);
int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
LogUtils.d(TAG, "drawEnergyStyle: 绘制完成顶部坐标=" + top);
LogUtils.d(TAG, "drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
}
/**
* 条纹风格绘制(分段条纹,扩展风格)
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/
private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) {
int stripeHeight = height / 100; // 单条条纹高度(均匀拆分)
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
// 从底部向上绘制对应电量条纹
for (int i = 0; i < battery; i++) {
int bottom = height - (stripeHeight * i);
int top = bottom - stripeHeight;
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
}
LogUtils.d(TAG, "drawStripeStyle: 绘制完成条纹数量=" + battery);
LogUtils.d(TAG, "drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
// ====================== 对外暴露方法(业务控制入口,按功能排序) ======================
// ====================================== 对外暴露方法(业务控制入口,按功能排序) ======================================
/**
* 设置当前电量(外部核心调用入口)
* @param value 电量值0-100
*/
public void setBatteryValue(int value) {
LogUtils.d(TAG, "setBatteryValue: 电量更新旧值=" + mBatteryValue + "新值=" + value);
LogUtils.d(TAG, "setBatteryValue电量更新 | 旧值=" + mBatteryValue + " | 新值=" + value);
mBatteryValue = value;
invalidateSelf(); // 触发重绘确保UI实时更新
LogUtils.d(TAG, "【setBatteryValue】已触发重绘");
}
/**
@@ -131,9 +160,10 @@ public class BatteryDrawable extends Drawable {
* @param isEnergyStyle true=能量风格false=条纹风格
*/
public void switchDrawStyle(boolean isEnergyStyle) {
LogUtils.d(TAG, "switchDrawStyle: 风格切换,旧=" + (mIsEnergyStyle ? "能量" : "条纹") + ",新=" + (isEnergyStyle ? "能量" : "条纹"));
LogUtils.d(TAG, "switchDrawStyle风格切换 | 旧风格=" + (mIsEnergyStyle ? "能量" : "条纹") + " | 新风格=" + (isEnergyStyle ? "能量" : "条纹"));
mIsEnergyStyle = isEnergyStyle;
invalidateSelf();
LogUtils.d(TAG, "【switchDrawStyle】已触发重绘");
}
/**
@@ -141,31 +171,42 @@ public class BatteryDrawable extends Drawable {
* @param color 新颜色值
*/
public void updateBatteryColor(int color) {
LogUtils.d(TAG, "updateBatteryColor: 颜色更新,旧=" + Integer.toHexString(mBatteryPaint.getColor()) + ",新=" + Integer.toHexString(color));
String oldColor = Integer.toHexString(mBatteryPaint.getColor());
String newColor = Integer.toHexString(color);
LogUtils.d(TAG, "【updateBatteryColor】颜色更新 | 旧颜色=" + oldColor + " | 新颜色=" + newColor);
mBatteryPaint.setColor(color);
invalidateSelf();
LogUtils.d(TAG, "【updateBatteryColor】已触发重绘");
}
// ====================== Getter方法按需暴露简洁无冗余 ======================
// ====================================== Getter方法按需暴露简洁无冗余 ======================================
/**
* 获取当前电量
* @return 电量值0-100-1=未初始化)
*/
public int getBatteryValue() {
return mBatteryValue;
}
/**
* 获取当前绘制风格
* @return true=能量风格false=条纹风格
*/
public boolean isEnergyStyle() {
return mIsEnergyStyle;
}
// ====================== Drawable抽象方法必须实现精简逻辑 ======================
// ====================================== Drawable抽象方法必须实现精简逻辑 ======================================
@Override
public void setAlpha(int alpha) {
LogUtils.d(TAG, "setAlpha: 透明度更新,旧=" + mBatteryPaint.getAlpha() + ",新=" + alpha);
LogUtils.d(TAG, "setAlpha透明度更新 | 旧值=" + mBatteryPaint.getAlpha() + " | 新值=" + alpha);
mBatteryPaint.setAlpha(alpha);
invalidateSelf();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
LogUtils.d(TAG, "setColorFilter: 设置颜色过滤filter=" + colorFilter);
LogUtils.d(TAG, "setColorFilter设置颜色过滤 | filter=" + colorFilter);
mBatteryPaint.setColorFilter(colorFilter);
invalidateSelf();
}

View File

@@ -22,24 +22,26 @@ import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
* @Describe 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
*/
public class MainContentView {
// ======================== 静态常量(置顶,唯一标识)========================
// ====================================== 静态常量区(唯一标识,变更类型分类) ======================================
public static final String TAG = "MainContentView";
// 变更类型常量(区分不同控件,精准处理逻辑)
private static final int CHANGE_TYPE_CHARGE_SWITCH = 1;
private static final int CHANGE_TYPE_USAGE_SWITCH = 2;
private static final int CHANGE_TYPE_SERVICE_SWITCH = 3;
private static final int CHANGE_TYPE_CHARGE_SEEKBAR = 4;
private static final int CHANGE_TYPE_USAGE_SEEKBAR = 5;
// 电量范围常量
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// ======================== 内部静态类(临时数据载体,避免外部依赖)========================
// ====================================== 内部静态类(临时数据载体,避免外部依赖) ======================================
/**
* 临时配置数据实体(缓存变更信息,取消时恢复)
*/
@@ -65,7 +67,7 @@ public class MainContentView {
}
}
// ======================== 事件回调接口(解耦视图与业务,提升扩展性)========================
// ====================================== 事件回调接口(解耦视图与业务,提升扩展性) ======================================
public interface OnViewActionListener {
void onChargeReminderSwitchChanged(boolean isChecked);
void onUsageReminderSwitchChanged(boolean isChecked);
@@ -74,17 +76,17 @@ public class MainContentView {
void onUsageReminderProgressChanged(int progress);
}
// ======================== 成员变量(按功能分类,避免混乱)========================
// ====================================== 成员变量(按功能分类,final优先避免混乱 ======================================
// 外部依赖实例(生命周期关联,优先声明)
private Context mContext;
private AppConfigUtils mAppConfigUtils;
private OnViewActionListener mActionListener;
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类)
// 视图控件(按「布局→开关→文本→进度条→图标」功能归类public控件标注用途
// 基础布局控件
public RelativeLayout mainLayout;
public MemoryCachedBackgroundView backgroundView;
LinearLayout mllBackgroundView;
private LinearLayout mllBackgroundView;
// 容器布局控件
public LinearLayout llLeftSeekBar;
public LinearLayout llRightSeekBar;
@@ -122,9 +124,9 @@ public class MainContentView {
// 对话框状态锁(避免快速点击重复弹窗)
private boolean isDialogShowing = false;
// ======================== 构造方法(初始化入口,逻辑闭环)========================
// ====================================== 构造方法(初始化入口,逻辑闭环) ======================================
public MainContentView(Context context, View rootView, OnViewActionListener actionListener) {
LogUtils.d(TAG, "MainContentView() | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener);
LogUtils.d(TAG, "MainContentView】构造器调用 | context=" + context + " | rootView=" + rootView + " | actionListener=" + actionListener);
// 初始化外部依赖
this.mContext = context;
this.mActionListener = actionListener;
@@ -136,24 +138,25 @@ public class MainContentView {
initConfirmDialog();
bindViewListeners();
LogUtils.d(TAG, "MainContentView 初始化完成");
LogUtils.d(TAG, "MainContentView初始化完成");
}
// ======================== 私有初始化方法(封装内部逻辑,仅暴露入口)========================
// ====================================== 私有初始化方法(封装内部逻辑,仅暴露入口) ======================================
/**
* 绑定视图控件(显式强转适配 Java7适配 API30 视图加载机制)
* @param rootView 根视图
*/
private void bindViews(View rootView) {
LogUtils.d(TAG, "bindViews() | rootView=" + rootView);
LogUtils.d(TAG, "bindViews】视图绑定开始 | rootView=" + rootView);
// 基础布局绑定
mainLayout = (RelativeLayout) rootView.findViewById(R.id.activitymainRelativeLayout1);
//backgroundView = (BackgroundView) rootView.findViewById(R.id.fragmentmainviewBackgroundView1);
mllBackgroundView = (LinearLayout) rootView.findViewById(R.id.ll_backgroundview);
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
if (backgroundView.getParent() != null) {
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
}
mllBackgroundView.addView(backgroundView);
backgroundView = App.sMemoryCachedBackgroundView.getLastInstance(mContext);
if (backgroundView.getParent() != null) {
((ViewGroup) backgroundView.getParent()).removeView(backgroundView);
LogUtils.d(TAG, "【bindViews】移除背景视图旧父容器");
}
mllBackgroundView.addView(backgroundView);
// 容器布局绑定
llLeftSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout1);
llRightSeekBar = (LinearLayout) rootView.findViewById(R.id.fragmentmainviewLinearLayout2);
@@ -177,17 +180,19 @@ public class MainContentView {
// 初始化进度缓存(从配置读取初始值)
mCurrentChargeProgress = mAppConfigUtils.getChargeReminderValue();
mCurrentUsageProgress = mAppConfigUtils.getUsageReminderValue();
LogUtils.d(TAG, "【bindViews】进度缓存初始化 | charge=" + mCurrentChargeProgress + " | usage=" + mCurrentUsageProgress);
// 关键视图绑定校验(仅保留核心控件错误日志,精简冗余)
if (mainLayout == null) LogUtils.e(TAG, "mainLayout 绑定失败");
if (backgroundView == null) LogUtils.e(TAG, "backgroundView 绑定失败");
if (mainLayout == null) LogUtils.e(TAG, "【bindViews】mainLayout 绑定失败");
if (backgroundView == null) LogUtils.e(TAG, "【bindViews】backgroundView 绑定失败");
LogUtils.d(TAG, "【bindViews】视图绑定完成");
}
/**
* 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染
*/
private void initBatteryDrawables() {
LogUtils.d(TAG, "initBatteryDrawables()");
LogUtils.d(TAG, "initBatteryDrawables】电池Drawable初始化开始");
// 当前电量 Drawable颜色从资源读取适配 API30 主题)
int colorCurrent = getResourceColor(R.color.colorCurrent);
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
@@ -197,15 +202,16 @@ public class MainContentView {
// 耗电提醒 Drawable
int colorUsage = getResourceColor(R.color.colorUsege);
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
}
/**
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
*/
private void initConfirmDialog() {
LogUtils.d(TAG, "initConfirmDialog()");
LogUtils.d(TAG, "initConfirmDialog】对话框初始化开始");
if (mContext == null) {
LogUtils.e(TAG, "Context 为空,初始化失败");
LogUtils.e(TAG, "【initConfirmDialog】Context 为空,初始化失败");
return;
}
@@ -245,16 +251,17 @@ public class MainContentView {
mConfigConfirmDialog = mDialogBuilder.create();
mConfigConfirmDialog.setCancelable(true);
mConfigConfirmDialog.setCanceledOnTouchOutside(true);
LogUtils.d(TAG, "【initConfirmDialog】对话框初始化完成");
}
/**
* 绑定视图事件监听Java7 显式实现接口,适配 API30 事件分发,修复进度条弹窗失效)
*/
private void bindViewListeners() {
LogUtils.d(TAG, "bindViewListeners()");
LogUtils.d(TAG, "bindViewListeners】事件监听绑定开始");
// 依赖校验,避免空指针
if (mAppConfigUtils == null || mActionListener == null || mConfigConfirmDialog == null) {
LogUtils.e(TAG, "依赖实例为空,跳过监听绑定");
if (mAppConfigUtils == null || mActionListener == null || mDialogBuilder == null) {
LogUtils.e(TAG, "【bindViewListeners】依赖实例为空,跳过监听绑定");
return;
}
@@ -267,14 +274,14 @@ public class MainContentView {
int originalValue = mAppConfigUtils.getChargeReminderValue();
// 进度无变化,不处理
if (originalValue == progress) {
LogUtils.d(TAG, "ChargeReminderSeekBar: 进度无变化,跳过");
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar: 进度无变化,跳过");
return;
}
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
}
@Override
@@ -289,7 +296,7 @@ public class MainContentView {
seekBar.setProgress(originalValue);
// 恢复进度缓存
mCurrentChargeProgress = originalValue;
LogUtils.d(TAG, "ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
}
});
@@ -305,6 +312,7 @@ public class MainContentView {
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(progress + "%");
}
LogUtils.d(TAG, "【bindViewListeners】ChargeReminderSeekBar实时更新 | 进度=" + progress);
}
}
@@ -314,7 +322,7 @@ public class MainContentView {
@Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
});
LogUtils.d(TAG, "充电提醒进度条专属监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】充电提醒进度条专属监听绑定完成");
}
// 充电提醒开关监听
@@ -330,10 +338,10 @@ public class MainContentView {
mTempConfigData = new TempConfigData(CHANGE_TYPE_CHARGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
LogUtils.d(TAG, "【bindViewListeners】cbEnableChargeReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "充电提醒开关监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】充电提醒开关监听绑定完成");
}
// 耗电提醒进度条监听(使用 VerticalSeekBar 专属接口确保弹窗100%触发)
@@ -345,14 +353,14 @@ public class MainContentView {
int originalValue = mAppConfigUtils.getUsageReminderValue();
// 进度无变化,不处理
if (originalValue == progress) {
LogUtils.d(TAG, "UsageReminderSeekBar: 进度无变化,跳过");
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar: 进度无变化,跳过");
return;
}
// 缓存变更数据,显示确认对话框
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SEEKBAR, originalValue, progress);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸抬起 | 原始值=" + originalValue + " | 新进度=" + progress);
}
@Override
@@ -367,7 +375,7 @@ public class MainContentView {
seekBar.setProgress(originalValue);
// 恢复进度缓存
mCurrentUsageProgress = originalValue;
LogUtils.d(TAG, "UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar触摸取消 | 进度回滚至=" + originalValue);
}
});
@@ -383,6 +391,7 @@ public class MainContentView {
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(progress + "%");
}
LogUtils.d(TAG, "【bindViewListeners】UsageReminderSeekBar实时更新 | 进度=" + progress);
}
}
@@ -392,7 +401,7 @@ public class MainContentView {
@Override
public void onStopTrackingTouch(VerticalSeekBar seekBar) {}
});
LogUtils.d(TAG, "耗电提醒进度条专属监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】耗电提醒进度条专属监听绑定完成");
}
// 耗电提醒开关监听
@@ -408,10 +417,10 @@ public class MainContentView {
mTempConfigData = new TempConfigData(CHANGE_TYPE_USAGE_SWITCH, originalValue, newValue);
updateDialogMessageByChangeType();
showConfigConfirmDialog();
LogUtils.d(TAG, "cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
LogUtils.d(TAG, "【bindViewListeners】cbEnableUsageReminder点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "耗电提醒开关监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】耗电提醒开关监听绑定完成");
}
// 服务总开关监听(核心优化:逻辑与其他控件完全对齐)
@@ -430,24 +439,24 @@ public class MainContentView {
updateDialogMessageByChangeType();
// 显示确认对话框
showConfigConfirmDialog();
LogUtils.d(TAG, "swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
LogUtils.d(TAG, "【bindViewListeners】swEnableService点击 | 原始值=" + originalValue + " | 变更后=" + newValue);
}
});
LogUtils.d(TAG, "服务总开关监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】服务总开关监听绑定完成");
}
LogUtils.d(TAG, "所有事件监听绑定完成");
LogUtils.d(TAG, "【bindViewListeners】所有事件监听绑定完成");
}
// ======================== 对外暴露核心方法(业务入口,精简参数,明确职责)========================
// ====================================== 对外暴露核心方法(业务入口,精简参数,明确职责) ======================================
/**
* 更新所有视图数据(从配置读取数据,统一刷新 UI适配 API30 视图更新规范)
* @param frameDrawable 进度条背景 Drawable外部传入适配主题切换
*/
public void updateViewData(Drawable frameDrawable) {
LogUtils.d(TAG, "updateViewData() | frameDrawable=" + frameDrawable);
LogUtils.d(TAG, "updateViewData】视图数据更新开始 | frameDrawable=" + frameDrawable);
if (mAppConfigUtils == null) {
LogUtils.e(TAG, "AppConfigUtils 为空,跳过更新");
LogUtils.e(TAG, "【updateViewData】AppConfigUtils 为空,跳过更新");
return;
}
@@ -462,12 +471,13 @@ public class MainContentView {
// 更新进度缓存
mCurrentChargeProgress = chargeVal;
mCurrentUsageProgress = usageVal;
LogUtils.d(TAG, "配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable);
LogUtils.d(TAG, "【updateViewData】配置数据读取完成 | charge=" + chargeVal + " | usage=" + usageVal + " | current=" + currentVal + " | serviceEnable=" + serviceEnable);
// 进度条背景更新
if (frameDrawable != null) {
if (llLeftSeekBar != null) llLeftSeekBar.setBackground(frameDrawable);
if (llRightSeekBar != null) llRightSeekBar.setBackground(frameDrawable);
LogUtils.d(TAG, "【updateViewData】进度条背景更新完成");
}
// 当前电量更新(联动 BatteryDrawable实时刷新图标
@@ -479,6 +489,7 @@ public class MainContentView {
tvCurrentBatteryValue.setTextColor(getResourceColor(R.color.colorCurrent));
tvCurrentBatteryValue.setText(currentVal + "%");
}
LogUtils.d(TAG, "【updateViewData】当前电量更新完成");
// 充电提醒视图更新
if (ivChargeReminderBattery != null && mChargeReminderBatteryDrawable != null) {
@@ -491,6 +502,7 @@ public class MainContentView {
}
if (sbChargeReminder != null) sbChargeReminder.setProgress(chargeVal);
if (cbEnableChargeReminder != null) cbEnableChargeReminder.setChecked(chargeEnable);
LogUtils.d(TAG, "【updateViewData】充电提醒视图更新完成");
// 耗电提醒视图更新
if (ivUsageReminderBattery != null && mUsageReminderBatteryDrawable != null) {
@@ -503,6 +515,7 @@ public class MainContentView {
}
if (sbUsageReminder != null) sbUsageReminder.setProgress(usageVal);
if (cbEnableUsageReminder != null) cbEnableUsageReminder.setChecked(usageEnable);
LogUtils.d(TAG, "【updateViewData】耗电提醒视图更新完成");
// 服务开关+提示文本更新(确保状态准确)
if (swEnableService != null) {
@@ -510,8 +523,9 @@ public class MainContentView {
swEnableService.setText(mContext.getString(R.string.txt_aboveswitch));
}
if (tvTips != null) tvTips.setText(mContext.getString(R.string.txt_aboveswitchtips));
LogUtils.d(TAG, "【updateViewData】服务开关与提示文本更新完成");
LogUtils.d(TAG, "所有视图数据更新完成");
LogUtils.d(TAG, "【updateViewData】所有视图数据更新完成");
}
/**
@@ -519,28 +533,28 @@ public class MainContentView {
* @param value 电量值(自动校准 0-100避免异常值
*/
public void updateCurrentBattery(int value) {
LogUtils.d(TAG, "updateCurrentBattery() | 原始值=" + value);
LogUtils.d(TAG, "updateCurrentBattery】当前电量更新开始 | 原始值=" + value);
// 核心依赖校验
if (tvCurrentBatteryValue == null || mCurrentBatteryDrawable == null || ivCurrentBattery == null) {
LogUtils.e(TAG, "视图/Drawable 为空,跳过更新");
LogUtils.e(TAG, "【updateCurrentBattery】视图/Drawable 为空,跳过更新");
return;
}
// 校准电量范围(强制 0-100防止 API30 视图显示异常)
int validValue = Math.max(0, Math.min(value, 100));
int validValue = Math.max(BATTERY_MIN, Math.min(value, BATTERY_MAX));
// 联动 BatteryDrawable 更新图标,同步文本显示
mCurrentBatteryDrawable.setBatteryValue(validValue);
ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
tvCurrentBatteryValue.setText(validValue + "%");
LogUtils.d(TAG, "更新完成 | 校准后值=" + validValue);
LogUtils.d(TAG, "【updateCurrentBattery】更新完成 | 校准后值=" + validValue);
}
/**
* 释放资源(主动回收,适配 API30 资源管控机制,优化小米手机内存占用)
*/
public void releaseResources() {
LogUtils.d(TAG, "releaseResources()");
LogUtils.d(TAG, "releaseResources】资源释放开始");
// 释放对话框资源(安全销毁,避免内存泄漏)
if (mConfigConfirmDialog != null) {
if (mConfigConfirmDialog.isShowing()) {
@@ -563,6 +577,7 @@ public class MainContentView {
// 置空视图实例(断开视图引用,辅助 GC 回收)
mainLayout = null;
backgroundView = null;
mllBackgroundView = null;
llLeftSeekBar = null;
llRightSeekBar = null;
cbEnableChargeReminder = null;
@@ -583,7 +598,7 @@ public class MainContentView {
mAppConfigUtils = null;
mActionListener = null;
LogUtils.d(TAG, "所有资源释放完成");
LogUtils.d(TAG, "【releaseResources】所有资源释放完成");
}
/**
@@ -591,7 +606,7 @@ public class MainContentView {
* @param enabled 服务启用状态
*/
public void setServiceSwitchChecked(boolean enabled) {
LogUtils.d(TAG, "setServiceSwitchChecked() | enabled=" + enabled);
LogUtils.d(TAG, "setServiceSwitchChecked】服务开关状态设置 | enabled=" + enabled);
if (swEnableService != null) {
swEnableService.setChecked(enabled);
}
@@ -602,33 +617,33 @@ public class MainContentView {
* @param enabled 是否允许点击
*/
public void setServiceSwitchEnabled(boolean enabled) {
LogUtils.d(TAG, "setServiceSwitchEnabled() | enabled=" + enabled);
LogUtils.d(TAG, "setServiceSwitchEnabled】服务开关点击状态设置 | enabled=" + enabled);
if (swEnableService != null) {
swEnableService.setEnabled(enabled);
}
}
// ======================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑)========================
// ====================================== 内部核心逻辑方法(对话框相关,封装确认/取消逻辑) ======================================
/**
* 显示配置变更确认对话框(确保 Activity 处于前台,避免异常,防止重复弹窗)
*/
private void showConfigConfirmDialog() {
LogUtils.d(TAG, "showConfigConfirmDialog() | isDialogShowing=" + isDialogShowing);
LogUtils.d(TAG, "showConfigConfirmDialog】对话框显示开始 | isDialogShowing=" + isDialogShowing);
// 对话框状态锁:正在显示则跳过,避免重复触发
if (isDialogShowing) {
LogUtils.d(TAG, "对话框已显示,跳过重复调用");
LogUtils.d(TAG, "【showConfigConfirmDialog】对话框已显示,跳过重复调用");
return;
}
// 基础校验:对话框/上下文/Builder 为空
if (mDialogBuilder == null || mContext == null) {
LogUtils.e(TAG, "对话框Builder/上下文异常,无法显示");
LogUtils.e(TAG, "【showConfigConfirmDialog】对话框Builder/上下文异常,无法显示");
if (mTempConfigData != null) cancelConfigChange();
return;
}
// Activity 状态校验:避免销毁后弹窗崩溃(适配 API30
Activity activity = (Activity) mContext;
if (activity.isFinishing() || activity.isDestroyed()) {
LogUtils.e(TAG, "Activity 已销毁,无法显示对话框");
LogUtils.e(TAG, "【showConfigConfirmDialog】Activity 已销毁,无法显示对话框");
if (mTempConfigData != null) cancelConfigChange();
return;
}
@@ -645,16 +660,16 @@ public class MainContentView {
mConfigConfirmDialog.setOnDismissListener(null);
}
});
LogUtils.d(TAG, "确认对话框显示成功");
LogUtils.d(TAG, "【showConfigConfirmDialog】确认对话框显示成功");
}
/**
* 确认配置变更(保存数据+回调监听+更新视图)
*/
private void confirmConfigChange() {
LogUtils.d(TAG, "confirmConfigChange() | mTempConfigData=" + mTempConfigData);
LogUtils.d(TAG, "confirmConfigChange】配置确认开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null || mActionListener == null) {
LogUtils.e(TAG, "依赖数据为空,确认失败");
LogUtils.e(TAG, "【confirmConfigChange】依赖数据为空,确认失败");
return;
}
@@ -663,13 +678,13 @@ public class MainContentView {
case CHANGE_TYPE_CHARGE_SWITCH:
mAppConfigUtils.setChargeReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onChargeReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "【confirmConfigChange】充电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break;
// 耗电提醒开关
case CHANGE_TYPE_USAGE_SWITCH:
mAppConfigUtils.setUsageReminderEnabled(mTempConfigData.newBooleanValue);
mActionListener.onUsageReminderSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒开关确认 | 值=" + mTempConfigData.newBooleanValue);
break;
// 服务总开关(核心:持久化配置+触发 Activity 回调)
case CHANGE_TYPE_SERVICE_SWITCH:
@@ -681,36 +696,37 @@ public class MainContentView {
}
// 2. 强制触发 Activity 回调,执行服务启停逻辑
mActionListener.onServiceSwitchChanged(mTempConfigData.newBooleanValue);
LogUtils.d(TAG, "服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
LogUtils.d(TAG, "【confirmConfigChange】服务开关确认 | 值=" + mTempConfigData.newBooleanValue + ",已持久化配置");
break;
// 充电提醒进度条
case CHANGE_TYPE_CHARGE_SEEKBAR:
mAppConfigUtils.setChargeReminderValue(mTempConfigData.newIntValue);
mActionListener.onChargeReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
LogUtils.d(TAG, "【confirmConfigChange】充电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break;
// 耗电提醒进度条
case CHANGE_TYPE_USAGE_SEEKBAR:
mAppConfigUtils.setUsageReminderValue(mTempConfigData.newIntValue);
mActionListener.onUsageReminderProgressChanged(mTempConfigData.newIntValue);
LogUtils.d(TAG, "耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
LogUtils.d(TAG, "【confirmConfigChange】耗电提醒进度确认 | 值=" + mTempConfigData.newIntValue);
break;
default:
LogUtils.w(TAG, "未知变更类型,跳过");
LogUtils.w(TAG, "【confirmConfigChange】未知变更类型,跳过");
break;
}
// 确认完成,清空临时数据
mTempConfigData = null;
LogUtils.d(TAG, "【confirmConfigChange】配置确认完成");
}
/**
* 取消配置变更(恢复原始值+刷新视图,确保 UI 与配置一致)
*/
private void cancelConfigChange() {
LogUtils.d(TAG, "cancelConfigChange() | mTempConfigData=" + mTempConfigData);
LogUtils.d(TAG, "cancelConfigChange】配置取消开始 | mTempConfigData=" + mTempConfigData);
if (mTempConfigData == null || mAppConfigUtils == null) {
LogUtils.e(TAG, "依赖数据为空,取消失败");
LogUtils.e(TAG, "【cancelConfigChange】依赖数据为空,取消失败");
return;
}
@@ -719,19 +735,19 @@ public class MainContentView {
if (cbEnableChargeReminder != null) {
cbEnableChargeReminder.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
LogUtils.d(TAG, "【cancelConfigChange】充电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_USAGE_SWITCH:
if (cbEnableUsageReminder != null) {
cbEnableUsageReminder.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_SERVICE_SWITCH:
if (swEnableService != null) {
swEnableService.setChecked(mTempConfigData.originalBooleanValue);
}
LogUtils.d(TAG, "服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
LogUtils.d(TAG, "【cancelConfigChange】服务开关取消 | 恢复值=" + mTempConfigData.originalBooleanValue);
break;
case CHANGE_TYPE_CHARGE_SEEKBAR:
if (sbChargeReminder != null) {
@@ -742,7 +758,7 @@ public class MainContentView {
ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
tvChargeReminderValue.setText(mTempConfigData.originalIntValue + "%");
}
LogUtils.d(TAG, "充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
LogUtils.d(TAG, "【cancelConfigChange】充电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break;
case CHANGE_TYPE_USAGE_SEEKBAR:
if (sbUsageReminder != null) {
@@ -753,22 +769,23 @@ public class MainContentView {
ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
tvUsageReminderValue.setText(mTempConfigData.originalIntValue + "%");
}
LogUtils.d(TAG, "耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
LogUtils.d(TAG, "【cancelConfigChange】耗电提醒进度取消 | 恢复值=" + mTempConfigData.originalIntValue);
break;
default:
LogUtils.w(TAG, "未知变更类型,跳过");
LogUtils.w(TAG, "【cancelConfigChange】未知变更类型,跳过");
break;
}
// 取消完成,清空临时数据
mTempConfigData = null;
LogUtils.d(TAG, "【cancelConfigChange】配置取消完成");
}
/**
* 根据变更类型更新对话框提示语(核心优化:通过 Builder 更新,确保生效)
*/
private void updateDialogMessageByChangeType() {
LogUtils.d(TAG, "updateDialogMessageByChangeType() | mTempConfigData=" + mTempConfigData);
LogUtils.d(TAG, "updateDialogMessageByChangeType】对话框消息更新开始 | mTempConfigData=" + mTempConfigData);
if (mDialogBuilder == null || mTempConfigData == null) return;
String message;
if (mTempConfigData.changeType == CHANGE_TYPE_SERVICE_SWITCH) {
@@ -782,42 +799,20 @@ public class MainContentView {
}
// 通过 Builder 设置消息,确保弹窗显示最新内容
mDialogBuilder.setMessage(message);
LogUtils.d(TAG, "【updateDialogMessageByChangeType】对话框消息更新完成 | message=" + message);
}
// ======================== 内部工具方法(封装重复逻辑,提升复用性)========================
/**
* 实时计算并更新比值预览sbUsageReminder / sbChargeReminder
* 处理除数为0的情况避免崩溃
*/
// private void updateRatioPreview() {
// if (mTvRatioPreview == null) return;
// float ratio;
// // 处理除数为0充电进度为0时显示0可根据需求改为“--”)
// if (mCurrentChargeProgress == 0) {
// ratio = 0.0f;
// } else {
// ratio = (float) mCurrentUsageProgress / mCurrentChargeProgress;
// }
// // 格式化比值保留1位小数适配本地化解决小米手机小数分隔符问题
// String ratioText = String.format(Locale.getDefault(), "比值:%.1f", ratio);
// mTvRatioPreview.setText(ratioText);
// // 触发比值变化回调
// if (mActionListener != null) {
// mActionListener.onRatioChanged(ratio);
// }
// LogUtils.d(TAG, "比值预览更新 | usage=" + mCurrentUsageProgress + " | charge=" + mCurrentChargeProgress + " | ratio=" + ratio);
// }
// ====================================== 内部工具方法(封装重复逻辑,提升复用性) ======================================
/**
* 获取资源颜色(适配 API30 主题颜色读取机制,兼容低版本,优化小米机型颜色显示,防御空指针)
* @param colorResId 颜色资源 ID
* @return 校准后的颜色值
*/
private int getResourceColor(int colorResId) {
LogUtils.d(TAG, "getResourceColor() | colorResId=" + colorResId);
LogUtils.d(TAG, "getResourceColor】资源颜色获取 | colorResId=" + colorResId);
// 空指针防御Context 为空返回默认黑色
if (mContext == null) {
LogUtils.e(TAG, "Context 为空,返回默认黑色");
LogUtils.e(TAG, "【getResourceColor】Context 为空,返回默认黑色");
return 0xFF000000;
}
// 适配 API30 主题颜色读取
@@ -833,11 +828,11 @@ public class MainContentView {
* @return 服务启用状态true=启用false=禁用)
*/
private boolean getServiceEnableState() {
LogUtils.d(TAG, "getServiceEnableState()");
LogUtils.d(TAG, "getServiceEnableState】服务状态获取开始");
ControlCenterServiceBean serviceBean = ControlCenterServiceBean.loadBean(mContext, ControlCenterServiceBean.class);
// 本地无配置时,默认禁用服务(与服务初始化逻辑对齐)
boolean state = serviceBean != null && serviceBean.isEnableService();
LogUtils.d(TAG, "服务启用状态获取完成 | state=" + state);
LogUtils.d(TAG, "【getServiceEnableState】服务启用状态获取完成 | state=" + state);
return state;
}
}

View File

@@ -8,41 +8,44 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BackgroundBean;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/21 20:43
* @Describe 单实例缓存版背景视图控件基于Java7- 强制缓存版
* 单实例缓存版背景视图控件基于Java7- 强制缓存版
* 核心:通过静态属性保存当前缓存路径和实例,支持强制重载图片
* 新增SP持久化最后加载路径、获取最后加载实例功能
* 强制缓存策略:无论内存是否紧张,不自动清理任何缓存实例和路径记录
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/21 20:43
*/
public class MemoryCachedBackgroundView extends BackgroundView {
// ====================================== 静态常量区TAG + SP相关常量 ======================================
public static final String TAG = "MemoryCachedBackgroundView";
// SP相关常量持久化最后加载路径
private static final String SP_NAME = "MemoryCachedBackgroundView_SP";
private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path";
// ====================================== 静态属性区(强制缓存核心:保存实例、路径、实例计数) ======================================
// 静态属性:保存当前缓存的路径和实例(强制保持,不自动销毁)
private static String sCachedImagePath;
private static MemoryCachedBackgroundView sCachedView;
// 新增:记录所有创建过的实例数量(用于强制缓存监控)
private static int sInstanceCount = 0;
// SP相关常量
private static final String SP_NAME = "MemoryCachedBackgroundView_SP";
private static final String KEY_LAST_LOAD_IMAGE_PATH = "last_load_image_path";
// ====================================== 构造器(继承并兼容父类) ======================================
// ====================================== 构造器(继承并兼容父类,私有构造防止外部实例化 ======================================
private MemoryCachedBackgroundView(Context context) {
super(context);
sInstanceCount++;
LogUtils.d(TAG, "构造器1创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
LogUtils.d(TAG, "构造器1创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
sInstanceCount++;
LogUtils.d(TAG, "构造器2创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
LogUtils.d(TAG, "构造器2创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
private MemoryCachedBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sInstanceCount++;
LogUtils.d(TAG, "构造器3创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
LogUtils.d(TAG, "构造器3创建MemoryCachedBackgroundView实例当前实例总数" + sInstanceCount);
}
// ====================================== 核心静态方法:获取/创建缓存实例(强制缓存版) ======================================
@@ -54,32 +57,32 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 缓存/新创建的MemoryCachedBackgroundView实例
*/
public static MemoryCachedBackgroundView getInstance(Context context, String imagePath, boolean isReload) {
LogUtils.d(TAG, "getInstance() 调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload + " | 当前实例总数:" + sInstanceCount);
LogUtils.d(TAG, "getInstance调用 | 图片路径:" + imagePath + " | 是否重载:" + isReload);
// 空路径校验
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "getInstance()图片路径为空,创建空实例");
LogUtils.e(TAG, "getInstance图片路径为空,创建空实例");
return new MemoryCachedBackgroundView(context);
}
// 1. 路径匹配缓存 → 判断是否强制重载
if (imagePath.equals(sCachedImagePath) && sCachedView != null) {
LogUtils.d(TAG, "getInstance()路径已缓存,当前缓存实例有效");
LogUtils.d(TAG, "getInstance路径已缓存,当前缓存实例有效");
if (isReload) {
LogUtils.d(TAG, "getInstance()强制重载图片 | " + imagePath);
LogUtils.d(TAG, "getInstance强制重载图片 | 路径:" + imagePath);
sCachedView.loadImage(imagePath);
} else {
LogUtils.d(TAG, "getInstance()使用缓存实例,无需重载 | " + imagePath);
LogUtils.d(TAG, "getInstance使用缓存实例,无需重载 | 路径:" + imagePath);
}
return sCachedView;
}
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心修改:保留旧实例,仅更新引用)
LogUtils.d(TAG, "getInstance()路径未缓存,新建实例(保留旧实例) | " + imagePath);
MemoryCachedBackgroundView oldView = sCachedView; // 保留旧实例引用,防止被销毁
// 2. 路径不匹配/无缓存 → 新建实例并更新静态缓存(核心:保留旧实例,仅更新引用)
LogUtils.d(TAG, "getInstance路径未缓存,新建实例(保留旧实例) | 路径:" + imagePath);
String oldPath = sCachedImagePath;
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = imagePath;
sCachedView.loadImage(imagePath);
LogUtils.d(TAG, "getInstance()已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)");
LogUtils.d(TAG, "getInstance已更新当前缓存实例,旧实例路径:" + oldPath + "(强制保持)");
return sCachedView;
}
@@ -91,28 +94,27 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 最后加载路径对应的实例
*/
public static MemoryCachedBackgroundView getLastInstance(Context context) {
LogUtils.d(TAG, "getLastInstance() 调用 | 当前实例总数:" + sInstanceCount);
LogUtils.d(TAG, "getLastInstance】调用");
// 1. 从SP获取最后加载的路径强制保持不自动删除
String lastPath = getLastLoadImagePath(context);
if (TextUtils.isEmpty(lastPath)) {
LogUtils.e(TAG, "getLastInstance()无最后加载路径,创建空实例");
LogUtils.e(TAG, "getLastInstance无最后加载路径,创建空实例");
return new MemoryCachedBackgroundView(context);
}
// 2. 路径匹配当前缓存 → 直接返回
if (lastPath.equals(sCachedImagePath) && sCachedView != null) {
LogUtils.d(TAG, "getLastInstance()使用最后路径缓存实例 | " + lastPath);
LogUtils.d(TAG, "getLastInstance使用最后路径缓存实例 | 路径:" + lastPath);
return sCachedView;
}
// 3. 路径不匹配 → 新建实例并更新缓存(保留旧实例)
LogUtils.d(TAG, "getLastInstance()最后路径未缓存,新建实例并加载(保留旧实例) | " + lastPath);
MemoryCachedBackgroundView oldView = sCachedView;
LogUtils.d(TAG, "getLastInstance最后路径未缓存,新建实例并加载(保留旧实例) | 路径:" + lastPath);
String oldPath = sCachedImagePath;
sCachedView = new MemoryCachedBackgroundView(context);
sCachedImagePath = lastPath;
sCachedView.loadImage(lastPath);
LogUtils.d(TAG, "getLastInstance()已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)");
LogUtils.d(TAG, "getLastInstance已更新最后路径实例,旧实例路径:" + oldPath + "(强制保持)");
return sCachedView;
}
@@ -124,11 +126,12 @@ public class MemoryCachedBackgroundView extends BackgroundView {
*/
private static void saveLastLoadImagePath(Context context, String imagePath) {
if (TextUtils.isEmpty(imagePath) || context == null) {
LogUtils.w(TAG, "【saveLastLoadImagePath】路径或上下文为空跳过保存");
return;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
sp.edit().putString(KEY_LAST_LOAD_IMAGE_PATH, imagePath).apply();
LogUtils.d(TAG, "saveLastLoadImagePath()已保存最后路径(强制保持) | " + imagePath);
LogUtils.d(TAG, "saveLastLoadImagePath已保存最后路径(强制保持) | 路径:" + imagePath);
}
/**
@@ -138,11 +141,12 @@ public class MemoryCachedBackgroundView extends BackgroundView {
*/
public static String getLastLoadImagePath(Context context) {
if (context == null) {
LogUtils.e(TAG, "【getLastLoadImagePath】上下文为空返回null");
return null;
}
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String lastPath = sp.getString(KEY_LAST_LOAD_IMAGE_PATH, null);
LogUtils.d(TAG, "getLastLoadImagePath()获取最后路径(强制保持) | " + lastPath);
LogUtils.d(TAG, "getLastLoadImagePath获取最后路径(强制保持) | 路径:" + lastPath);
return lastPath;
}
@@ -151,9 +155,8 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* 清除当前缓存实例和路径(强制缓存策略:仅日志,不实际清理)
*/
public static void clearCache() {
LogUtils.w(TAG, "clearCache() 调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath);
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearCache():强制缓存策略生效,未清除任何实例和路径");
LogUtils.w(TAG, "clearCache调用(强制缓存策略:不实际清理缓存) | 当前缓存路径:" + sCachedImagePath);
LogUtils.d(TAG, "【clearCache】强制缓存策略生效未清除任何实例和路径");
}
/**
@@ -161,22 +164,20 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @param imagePath 图片路径
*/
public static void removeCache(String imagePath) {
LogUtils.w(TAG, "removeCache() 调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath);
LogUtils.w(TAG, "removeCache调用(强制缓存策略:不实际清理缓存) | 图片路径:" + imagePath);
if (TextUtils.isEmpty(imagePath)) {
LogUtils.e(TAG, "removeCache()图片路径为空,清除失败");
LogUtils.e(TAG, "removeCache图片路径为空,清除失败");
return;
}
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "removeCache():强制缓存策略生效,未清除任何实例和路径");
LogUtils.d(TAG, "【removeCache】强制缓存策略生效未清除任何实例和路径");
}
/**
* 清除所有缓存(强制缓存策略:仅日志,不实际清理)
*/
public static void clearAllCache() {
LogUtils.w(TAG, "clearAllCache() 调用(强制缓存策略:不实际清理缓存)");
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearAllCache()强制缓存策略生效未清除任何实例、路径和SP记录");
LogUtils.w(TAG, "clearAllCache调用(强制缓存策略:不实际清理缓存)");
LogUtils.d(TAG, "【clearAllCache】强制缓存策略生效未清除任何实例、路径和SP记录");
}
/**
@@ -184,7 +185,9 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 存在返回true否则返回false
*/
public static boolean hasCache() {
return sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
boolean hasCache = sCachedView != null && !TextUtils.isEmpty(sCachedImagePath);
LogUtils.d(TAG, "【hasCache】缓存存在状态" + hasCache);
return hasCache;
}
/**
@@ -192,9 +195,8 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @param context 上下文
*/
public static void clearLastLoadImagePath(Context context) {
LogUtils.w(TAG, "clearLastLoadImagePath() 调用强制缓存策略不实际清理SP记录");
// 核心修改:注释所有清理逻辑,仅保留日志
LogUtils.d(TAG, "clearLastLoadImagePath()强制缓存策略生效未清除SP中最后路径记录");
LogUtils.w(TAG, "clearLastLoadImagePath调用强制缓存策略不实际清理SP记录");
LogUtils.d(TAG, "【clearLastLoadImagePath】强制缓存策略生效未清除SP中最后路径记录");
}
// ====================================== 辅助方法:从缓存获取上下文 ======================================
@@ -203,13 +205,15 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 上下文实例无则返回null
*/
private static Context getContextFromCache() {
return sCachedView != null ? sCachedView.getContext() : null;
Context context = sCachedView != null ? sCachedView.getContext() : null;
LogUtils.d(TAG, "【getContextFromCache】从缓存获取上下文" + context);
return context;
}
// ====================================== 重写父类方法:增强日志+SP持久化强制保持版 ======================================
@Override
public void loadImage(String imagePath) {
LogUtils.d(TAG, "loadImage() 重载方法调用 | 图片路径:" + imagePath);
LogUtils.d(TAG, "loadImage调用 | 图片路径:" + imagePath);
super.loadImage(imagePath);
// 保存最后加载路径到SP强制保持不自动删除
saveLastLoadImagePath(getContext(), imagePath);
@@ -217,13 +221,13 @@ public class MemoryCachedBackgroundView extends BackgroundView {
@Override
public void loadByBackgroundBean(BackgroundBean bean) {
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()));
LogUtils.d(TAG, "loadByBackgroundBean调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()));
super.loadByBackgroundBean(bean);
}
@Override
public void loadByBackgroundBean(BackgroundBean bean, boolean isRefresh) {
LogUtils.d(TAG, "loadBackgroundBean() 重载方法调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
LogUtils.d(TAG, "loadByBackgroundBean调用 | BackgroundBean" + (bean == null ? "null" : bean.toString()) + " | 是否刷新:" + isRefresh);
super.loadByBackgroundBean(bean, isRefresh);
}
@@ -233,7 +237,7 @@ public class MemoryCachedBackgroundView extends BackgroundView {
* @return 实例总数
*/
public static int getInstanceCount() {
LogUtils.d(TAG, "getInstanceCount() 调用 | 当前实例总数:" + sInstanceCount);
LogUtils.d(TAG, "getInstanceCount调用 | 当前实例总数:" + sInstanceCount);
return sInstanceCount;
}
}

View File

@@ -8,10 +8,10 @@ import android.widget.SeekBar;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 垂直进度条控件,适配 API30支持逆时针旋转0在下100在上
* 修复滑块同步+弹窗触发bug新增实时进度变化监听接口支持拖动时实时回调进度
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 14:11
* @Describe 垂直进度条控件,适配 API30支持逆时针旋转0在下100在上修复滑块同步+弹窗触发bug
* 新增:实时进度变化监听接口,支持拖动时实时回调进度
*/
public class VerticalSeekBar extends SeekBar {
// ======================== 静态常量 =========================
@@ -76,26 +76,26 @@ public class VerticalSeekBar extends SeekBar {
public VerticalSeekBar(Context context) {
super(context);
initView();
LogUtils.d(TAG, "VerticalSeekBar(Context) 初始化");
LogUtils.d(TAG, "【构造器1】VerticalSeekBar 初始化完成");
}
public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet) 初始化");
LogUtils.d(TAG, "【构造器2】VerticalSeekBar 初始化完成");
}
public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
LogUtils.d(TAG, "VerticalSeekBar(Context, AttributeSet, int) 初始化");
LogUtils.d(TAG, "【构造器3】VerticalSeekBar 初始化完成");
}
// ======================== 初始化方法 =========================
private void initView() {
// 移除水平默认阴影,优化垂直显示效果,减少 API30 不必要的绘制开销
setBackgroundDrawable(null);
LogUtils.d(TAG, "initView: 移除默认背景阴影,完成视图初始化");
LogUtils.d(TAG, "initView移除默认背景阴影,完成视图初始化");
}
// ======================== 对外设置方法(监听接口绑定)========================
@@ -105,7 +105,7 @@ public class VerticalSeekBar extends SeekBar {
*/
public void setOnVerticalSeekBarTouchListener(OnVerticalSeekBarTouchListener listener) {
this.mTouchListener = listener;
LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener: 触摸监听器绑定完成");
LogUtils.d(TAG, "setOnVerticalSeekBarTouchListener触摸监听器绑定完成");
}
/**
@@ -114,7 +114,7 @@ public class VerticalSeekBar extends SeekBar {
*/
public void setOnVerticalSeekBarChangeListener(OnVerticalSeekBarChangeListener listener) {
this.mProgressChangeListener = listener;
LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener: 实时进度监听器绑定完成");
LogUtils.d(TAG, "setOnVerticalSeekBarChangeListener实时进度监听器绑定完成");
}
// ======================== 重写系统方法(测量/布局/绘制)========================
@@ -122,13 +122,13 @@ public class VerticalSeekBar extends SeekBar {
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
LogUtils.v(TAG, "onMeasure: 垂直测量完成,宽=" + getMeasuredHeight() + ", 高=" + getMeasuredWidth());
LogUtils.v(TAG, "onMeasure垂直测量完成,宽=" + getMeasuredHeight() + "高=" + getMeasuredWidth());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
LogUtils.v(TAG, "onSizeChanged: 尺寸变化,新宽=" + h + ", 新高=" + w);
LogUtils.v(TAG, "onSizeChanged尺寸变化,新宽=" + h + "新高=" + w);
}
@Override
@@ -137,7 +137,7 @@ public class VerticalSeekBar extends SeekBar {
canvas.rotate(-90);
canvas.translate(-getHeight(), 0);
super.onDraw(canvas);
LogUtils.v(TAG, "onDraw: 完成垂直绘制,旋转角度=-90°");
LogUtils.v(TAG, "onDraw完成垂直绘制,旋转角度=-90°");
}
// ======================== 重写进度设置方法(修复滑块同步+新增实时回调)========================
@@ -151,10 +151,11 @@ public class VerticalSeekBar extends SeekBar {
// 强制触发尺寸变化同步刷新滑块位置核心bug修复逻辑
onSizeChanged(getWidth(), getHeight(), 0, 0);
mProgress = progress;
LogUtils.d(TAG, "setProgress: 进度设置为" + progress + ",滑块同步刷新");
LogUtils.d(TAG, "setProgress进度设置为" + progress + ",滑块同步刷新");
// 触发实时进度监听(外部调用 setProgress 时 fromUser 为 false
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, progress, false);
LogUtils.v(TAG, "【setProgress】触发实时进度回调fromUser=false");
}
}
@@ -165,20 +166,22 @@ public class VerticalSeekBar extends SeekBar {
super.onTouchEvent(event);
boolean handled = true; // 强制消费事件,避免事件被拦截导致回调丢失
boolean fromUser = true; // 标记是否是用户触摸导致的进度变化
int action = event.getAction();
switch (event.getAction()) {
switch (action) {
case MotionEvent.ACTION_DOWN:
LogUtils.d(TAG, "onTouchEvent: 触摸按下Y坐标=" + event.getY());
LogUtils.d(TAG, "onTouchEvent触摸按下Y坐标=" + event.getY());
// 触发实时进度监听:开始触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onStartTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发开始触摸回调");
}
break;
case MotionEvent.ACTION_MOVE:
calculateProgress(event.getY());
setProgress(mProgress);
LogUtils.v(TAG, "onTouchEvent: 触摸滑动,进度更新为" + mProgress);
LogUtils.v(TAG, "onTouchEvent触摸滑动,进度更新为" + mProgress);
// 触发实时进度监听:进度变化
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
@@ -188,27 +191,31 @@ public class VerticalSeekBar extends SeekBar {
case MotionEvent.ACTION_UP:
calculateProgress(event.getY());
setProgress(mProgress);
LogUtils.d(TAG, "onTouchEvent: 触摸抬起,进度=" + mProgress + ",触发弹窗回调");
// 触发实时进度监听:停止触摸
LogUtils.d(TAG, "onTouchEvent触摸抬起,进度=" + mProgress + ",触发弹窗回调");
// 触发实时进度监听:进度变化+停止触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onProgressChanged(this, mProgress, fromUser);
mProgressChangeListener.onStopTrackingTouch(this);
LogUtils.v(TAG, "【onTouchEvent】触发停止触摸回调");
}
// 核心:调用原有触摸接口,通知外部触发配置变更对话框
if (mTouchListener != null) {
mTouchListener.onTouchUp(this, mProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸抬起回调");
}
break;
case MotionEvent.ACTION_CANCEL:
LogUtils.d(TAG, "onTouchEvent: 触摸取消,当前进度=" + getProgress());
int currentProgress = getProgress();
LogUtils.d(TAG, "【onTouchEvent】触摸取消当前进度=" + currentProgress);
// 触发实时进度监听:停止触摸
if (mProgressChangeListener != null) {
mProgressChangeListener.onStopTrackingTouch(this);
}
// 可选:触摸取消时回调,外部可做进度回滚处理
if (mTouchListener != null) {
mTouchListener.onTouchCancel(this, getProgress());
mTouchListener.onTouchCancel(this, currentProgress);
LogUtils.v(TAG, "【onTouchEvent】触发触摸取消回调");
}
break;
}
@@ -225,7 +232,7 @@ public class VerticalSeekBar extends SeekBar {
mProgress = getMax() - (int) (getMax() * touchY / getHeight());
// 校准进度范围,防止超出 0~100兼容 API30 进度边界校验)
mProgress = Math.max(0, Math.min(mProgress, getMax()));
LogUtils.v(TAG, "calculateProgress: 触摸Y=" + touchY + ",计算进度=" + mProgress);
LogUtils.v(TAG, "calculateProgress触摸Y=" + touchY + ",计算进度=" + mProgress + ",校准后=" + mProgress);
}
}

View File

@@ -22,4 +22,5 @@
<string name="subtitle_activity_pixelpicker">背景像素拾取</string>
<string name="subtitle_activity_about">关于应用</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;向右滑动100%可以清理电量记录。&gt;&gt;&gt;</string>
<string name="msg_no_battery_record">无电池记录数据</string>
</resources>

View File

@@ -31,6 +31,7 @@
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
<string name="subtitle_activity_about">About The APP</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string>
<string name="msg_no_battery_record">No Battery Record</string>
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
<string name="permission_title">权限申请</string>