重构权限申请模块,图片选择剪裁部分测试通过。
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Dec 01 07:58:43 GMT 2025
|
||||
#Mon Dec 01 09:33:34 GMT 2025
|
||||
stageCount=13
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.12
|
||||
buildCount=52
|
||||
buildCount=59
|
||||
baseBetaVersion=15.11.13
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,23 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import android.util.Log;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.App;
|
||||
import cc.winboll.studio.powerbell.BuildConfig;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -570,7 +579,7 @@ public class BackgroundSourceUtils {
|
||||
}
|
||||
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 适配原调用:mBgSourceUtils.copyFile(new File(""), parentDir)
|
||||
* 核心:复用FileUtils,支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景
|
||||
@@ -620,5 +629,184 @@ public class BackgroundSourceUtils {
|
||||
return "外部存储目录(非应用私有,权限受限)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ======================================== 核心实现:获取图片旋转角度 ========================================
|
||||
/**
|
||||
* 读取图片EXIF信息,获取旋转角度(适配JPEG/PNG等主流格式)
|
||||
* @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录)
|
||||
* @return 旋转角度(0/90/180/270,无旋转返回0)
|
||||
*/
|
||||
public int getImageRotateAngle(String imagePath) {
|
||||
// 1. 入参校验(避免空指针/无效路径)
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
Log.e(TAG, "getImageRotateAngle: 图片路径为空");
|
||||
return 0;
|
||||
}
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
// 2. 读取图片EXIF信息(优先用流读取,避免文件占用)
|
||||
inputStream = new FileInputStream(imageFile);
|
||||
ExifInterface exifInterface = new ExifInterface(inputStream);
|
||||
|
||||
// 3. 获取旋转角度标签(兼容不同设备的EXIF字段)
|
||||
int orientation = exifInterface.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL
|
||||
);
|
||||
|
||||
// 4. 解析旋转角度(标准EXIF角度映射)
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default: // 正常/翻转等其他情况,均视为0度
|
||||
return 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 兼容异常场景:如图片无EXIF信息、格式不支持(如WebP)
|
||||
Log.w(TAG, "getImageRotateAngle: 读取EXIF异常,路径:" + imagePath + ",错误:" + e.getMessage());
|
||||
return 0;
|
||||
} finally {
|
||||
// 5. 关闭流资源(避免内存泄漏/文件占用)
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ======================================== 图片处理核心方法(压缩/裁剪/保存) ========================================
|
||||
/**
|
||||
* 压缩图片并保存(核心修复:路径非空校验+兜底路径,Java7 手动管理流)
|
||||
*/
|
||||
public void compressQualityToRecivedPicture(Bitmap bitmap) {
|
||||
// 兼容裁剪等旧调用:从工具类获取默认压缩路径,转发至重载函数
|
||||
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
|
||||
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:指定路径压缩图片并保存(修复:压缩后同步路径到预览Bean)
|
||||
* 适配场景:裁剪后生成压缩图,强制绑定路径到预览Bean,避免路径错位
|
||||
* @param bitmap 待压缩的Bitmap(裁剪后的缩放图)
|
||||
* @param targetCompressPath 强制指定的压缩目标路径(从预览Bean获取/生成)
|
||||
*/
|
||||
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
|
||||
LogUtils.d(TAG, "【压缩启动】开始压缩图片(指定路径),Bitmap状态:" + (bitmap != null && !bitmap.isRecycled()));
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
ToastUtils.show("压缩失败:图片为空");
|
||||
LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收");
|
||||
return;
|
||||
}
|
||||
|
||||
OutputStream outStream = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
String scaledCompressFilePath = targetCompressPath;
|
||||
// 兜底:若传入路径为空,生成临时压缩路径(兼容异常场景)
|
||||
if (TextUtils.isEmpty(scaledCompressFilePath)) {
|
||||
LogUtils.e(TAG, "【压缩异常】指定路径为空,使用临时兜底路径");
|
||||
File tempDir = new File(App.getTempDirPath(), "PreviewCompress"); // 多包名环境下临时目录隔离
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs();
|
||||
FileUtils.copyFile(new File(""), tempDir); // 复用原有目录创建逻辑
|
||||
}
|
||||
scaledCompressFilePath = new File(tempDir, "preview_compress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath();
|
||||
LogUtils.d(TAG, "【压缩兜底】临时路径:" + scaledCompressFilePath);
|
||||
}
|
||||
|
||||
File compressFile = new File(scaledCompressFilePath);
|
||||
LogUtils.d(TAG, "【压缩配置】目标路径:" + scaledCompressFilePath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB");
|
||||
|
||||
// 确保父目录存在(兼容Android 10+分区存储,多包名路径适配)
|
||||
File parentDir = compressFile.getParentFile();
|
||||
if (parentDir == null) {
|
||||
LogUtils.e(TAG, "【压缩异常】父目录为空,无法创建文件");
|
||||
ToastUtils.show("压缩失败:路径无效");
|
||||
return;
|
||||
}
|
||||
if (!parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
FileUtils.copyFile(new File(""), parentDir);
|
||||
LogUtils.d(TAG, "【压缩准备】目录已创建:" + parentDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
// 清理旧的压缩文件(避免文件残留)
|
||||
if (compressFile.exists()) {
|
||||
clearOldFileByExternal(compressFile, "旧压缩文件");
|
||||
}
|
||||
compressFile.createNewFile();
|
||||
|
||||
// 写入压缩图(质量80,平衡清晰度和内存)
|
||||
fos = new FileOutputStream(compressFile);
|
||||
outStream = new BufferedOutputStream(fos);
|
||||
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
|
||||
outStream.flush();
|
||||
// 强制同步到磁盘(避免异步写入导致控件读取不到文件)
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.getFD().sync();
|
||||
LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘");
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "【压缩保存】sync()失败,flush()兜底:" + e.getMessage());
|
||||
outStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + compressFile.length() / 1024 + "KB");
|
||||
|
||||
// 关键修复:压缩成功后,强制同步路径到预览Bean(双重保障,避免时序错位)
|
||||
if (compressSuccess) {
|
||||
BackgroundBean previewBean = getPreviewBackgroundBean();
|
||||
if (previewBean != null) {
|
||||
previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath);
|
||||
saveSettings(); // 持久化配置,多包名环境下配置隔离
|
||||
LogUtils.d(TAG, "【压缩路径同步】已绑定到预览Bean:" + scaledCompressFilePath);
|
||||
} else {
|
||||
LogUtils.e(TAG, "【压缩路径同步失败】预览Bean为空");
|
||||
}
|
||||
} else {
|
||||
ToastUtils.show("图片压缩失败");
|
||||
LogUtils.e(TAG, "【压缩失败】Bitmap.compress()返回false");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【压缩异常】IO错误:" + e.getMessage(), e);
|
||||
ToastUtils.show("图片压缩失败");
|
||||
} finally {
|
||||
// 资源回收(避免内存泄漏)
|
||||
if (outStream != null) {
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【流关闭失败】FileOutputStream:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.powerbell.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/01 16:05
|
||||
* @Describe 权限申请工具类(单例)
|
||||
* 核心特性:
|
||||
* 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容)
|
||||
* 2. 支持多包名场景(无硬编码包名)
|
||||
* 3. 统一权限校验、申请、回调处理
|
||||
* 4. 自带用户引导(拒绝权限+不再询问场景)
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static final String TAG = "PermissionUtils";
|
||||
// 存储权限请求码(与Activity保持一致,避免冲突)
|
||||
public static final int STORAGE_PERMISSION_REQUEST2 = 100;
|
||||
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
|
||||
|
||||
// 单例实例(双重校验锁,线程安全)
|
||||
private static volatile PermissionUtils sInstance;
|
||||
|
||||
// 私有构造(禁止外部实例化)
|
||||
private PermissionUtils() {}
|
||||
|
||||
/**
|
||||
* 获取单例实例(适配多包名,无硬编码)
|
||||
*/
|
||||
public static PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ======================================== 存储权限核心方法 ========================================
|
||||
/**
|
||||
* 检查并申请存储权限(统一入口,适配全Android版本)
|
||||
* @param activity 上下文(用于权限申请和弹窗)
|
||||
* @return true:权限已全部获取;false:需要申请权限
|
||||
*/
|
||||
public boolean checkAndRequestStoragePermission(Activity activity) {
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁,权限检查失败");
|
||||
return false;
|
||||
}
|
||||
LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT);
|
||||
|
||||
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号)
|
||||
String[] requiredPermissions = {
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
};
|
||||
|
||||
// 筛选未授予的权限
|
||||
List<String> needPermissions = new ArrayList<>();
|
||||
for (String permission : requiredPermissions) {
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
needPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
// 无权限需要申请:触发动态申请
|
||||
if (!needPermissions.isEmpty()) {
|
||||
String[] permissionsArr = needPermissions.toArray(new String[0]);
|
||||
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
|
||||
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 所有权限已授予
|
||||
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储权限申请回调(统一逻辑,无需在Activity中重复编写)
|
||||
* @param activity 上下文
|
||||
* @param requestCode 请求码(匹配STORAGE_PERMISSION_REQUEST)
|
||||
* @param permissions 申请的权限数组
|
||||
* @param grantResults 权限授予结果数组
|
||||
* @return true:回调已处理;false:非当前工具类的权限回调
|
||||
*/
|
||||
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
||||
// 过滤非存储权限回调
|
||||
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
|
||||
return false;
|
||||
}
|
||||
LogUtils.d(TAG, "【权限回调】处理存储权限回调,requestCode:" + requestCode);
|
||||
|
||||
if (activity == null || activity.isFinishing()) {
|
||||
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁,回调处理终止");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 校验所有权限是否授予
|
||||
boolean allGranted = true;
|
||||
for (int grantResult : grantResults) {
|
||||
if (grantResult != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allGranted) {
|
||||
// 全部授予:提示用户重新操作
|
||||
ToastUtils.show(activity.getString(R.string.permission_grant_success));
|
||||
LogUtils.d(TAG, "【权限回调】所有存储权限已授予");
|
||||
} else {
|
||||
// 部分/全部拒绝:判断是否勾选“不再询问”
|
||||
boolean shouldShowRationale = false;
|
||||
for (String permission : permissions) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
|
||||
shouldShowRationale = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowRationale) {
|
||||
// 未勾选“不再询问”:弹窗引导重新申请
|
||||
showPermissionRationaleDialog(activity);
|
||||
} else {
|
||||
// 已勾选“不再询问”:引导用户去设置页开启
|
||||
showPermissionSettingDialog(activity);
|
||||
}
|
||||
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ======================================== 辅助方法(私有,封装细节) ========================================
|
||||
/**
|
||||
* 弹窗:未勾选“不再询问”时,提示用户授予权限
|
||||
*/
|
||||
private void showPermissionRationaleDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.permission_title))
|
||||
.setMessage(activity.getString(R.string.permission_storage_rationale))
|
||||
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// 重新申请权限
|
||||
checkAndRequestStoragePermission(activity);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(activity.getString(R.string.cancel), null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
|
||||
*/
|
||||
private void showPermissionSettingDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.permission_denied_title))
|
||||
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
|
||||
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// 跳转应用设置页(适配多包名,动态获取当前包名)
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(activity.getString(R.string.cancel), null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// ======================================== 扩展:其他权限方法(可选) ========================================
|
||||
/**
|
||||
* 检查单个权限是否已授予(通用方法,可复用)
|
||||
*/
|
||||
public boolean isPermissionGranted(Activity activity, String permission) {
|
||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
||||
return false;
|
||||
}
|
||||
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请单个权限(通用方法,可复用)
|
||||
*/
|
||||
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
|
||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
||||
return;
|
||||
}
|
||||
if (!isPermissionGranted(activity, permission)) {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||
import java.io.File;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -97,6 +100,10 @@ public class BackgroundView extends RelativeLayout {
|
||||
this.addView(ivBackground); // 添加到父容器(控件本身)
|
||||
}
|
||||
|
||||
public void setPixelColor(int nBackgroundColor) {
|
||||
ivBackground.setBackgroundColor(nBackgroundColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【对外提供】重新加载正式背景图片(从正式Bean获取路径)
|
||||
* 用于:退出预览模式、恢复默认背景、正式背景更新后刷新
|
||||
@@ -111,31 +118,126 @@ public class BackgroundView extends RelativeLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* 【对外提供】重新加载预览背景图片(从预览Bean获取路径)
|
||||
* 修复:增加isUseBackgroundFile校验,确保启用背景图时才加载
|
||||
* 重新加载预览背景(修复:强制读取预览Bean最新路径+多重有效性校验)
|
||||
* 作用:控件加载图片时,优先用压缩图路径,失败则用原图路径,均失败则显示默认图
|
||||
*/
|
||||
public void reloadPreviewBackground() {
|
||||
LogUtils.d(TAG, "=== 开始重新加载预览背景 ===");
|
||||
isPreviewMode = true; // 标记为预览模式
|
||||
// 从工具类获取最新预览背景Bean
|
||||
BackgroundBean previewBean = backgroundSourceUtils.getPreviewBackgroundBean();
|
||||
// 修复:若未启用背景图,直接显示透明背景(避免无效加载)
|
||||
if (!previewBean.isUseBackgroundFile()) {
|
||||
LogUtils.d(TAG, "预览Bean未启用背景图(isUseBackgroundFile=false),显示透明背景");
|
||||
setDefaultTransparentBackground();
|
||||
LogUtils.d(TAG, "=== 开始加载预览背景(路径校验版)===");
|
||||
// 关键:直接从工具类获取预览Bean最新路径(避免使用缓存路径)
|
||||
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(getContext());
|
||||
BackgroundBean previewBean = bgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean == null) {
|
||||
LogUtils.e(TAG, "【加载失败】预览Bean为空,显示默认图");
|
||||
setDefaultBackground();
|
||||
return;
|
||||
}
|
||||
// 优先加载压缩图,无则加载原图(保持原逻辑)
|
||||
String backgroundPath;
|
||||
if (previewBean.isUseBackgroundScaledCompressFile()) {
|
||||
backgroundPath = backgroundSourceUtils.getPreviewBackgroundScaledCompressFilePath();
|
||||
} else {
|
||||
backgroundPath = backgroundSourceUtils.getPreviewBackgroundFilePath();
|
||||
|
||||
// 1. 优先加载压缩图路径(预览首选,节省内存)
|
||||
String compressPath = previewBean.getBackgroundScaledCompressFilePath();
|
||||
File compressFile = checkFileValidity(compressPath); // 校验文件有效性
|
||||
if (compressFile != null) {
|
||||
loadBitmapAndDisplay(compressPath, "压缩图");
|
||||
return;
|
||||
}
|
||||
// 加载并显示图片
|
||||
loadAndSetImageViewBackground(backgroundPath);
|
||||
|
||||
// 2. 兜底加载原图路径(压缩图失败时)
|
||||
LogUtils.w(TAG, "【压缩图无效】尝试加载原图路径");
|
||||
String originalPath = previewBean.getBackgroundFilePath();
|
||||
File originalFile = checkFileValidity(originalPath);
|
||||
if (originalFile != null) {
|
||||
loadBitmapAndDisplay(originalPath, "原图");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 终极兜底:显示默认图(避免白色)
|
||||
LogUtils.e(TAG, "【加载失败】压缩图和原图均无效,显示默认图");
|
||||
setDefaultBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:校验文件有效性(路径非空+文件存在+是文件+大小>100bytes)
|
||||
* @param filePath 待校验的图片路径
|
||||
* @return 有效则返回File对象,无效则返回null
|
||||
*/
|
||||
@Nullable
|
||||
private File checkFileValidity(String filePath) {
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
LogUtils.w(TAG, "【文件校验】路径为空");
|
||||
return null;
|
||||
}
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
LogUtils.w(TAG, "【文件校验】文件不存在:" + filePath);
|
||||
return null;
|
||||
}
|
||||
if (!file.isFile()) {
|
||||
LogUtils.w(TAG, "【文件校验】不是文件:" + filePath);
|
||||
return null;
|
||||
}
|
||||
if (file.length() <= 100) {
|
||||
LogUtils.w(TAG, "【文件校验】文件过小(无效):" + filePath + ",大小:" + file.length() + "bytes");
|
||||
return null;
|
||||
}
|
||||
LogUtils.d(TAG, "【文件校验】有效:" + filePath + ",大小:" + file.length() + "bytes");
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:加载Bitmap并显示(统一处理Bitmap有效性)
|
||||
* @param imagePath 图片路径
|
||||
* @param imageType 图片类型(压缩图/原图,用于日志区分)
|
||||
*/
|
||||
private void loadBitmapAndDisplay(String imagePath, String imageType) {
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
|
||||
// 校验Bitmap有效性(避免宽高为0的无效Bitmap)
|
||||
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
|
||||
LogUtils.e(TAG, "【" + imageType + "加载失败】Bitmap无效(空/宽高为0)");
|
||||
setDefaultBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 强制设置ScaleType(避免因缩放导致图片显示异常)
|
||||
ivBackground.setScaleType(ScaleType.CENTER_CROP);
|
||||
// 移除默认背景色(避免白色覆盖图片)
|
||||
setBackgroundColor(getResources().getColor(android.R.color.transparent));
|
||||
// 显示图片并调整尺寸(复用原有调整逻辑)
|
||||
ivBackground.setImageBitmap(bitmap);
|
||||
adjustImageViewSize(bitmap); // 复用项目中原有尺寸调整方法
|
||||
LogUtils.d(TAG, "【" + imageType + "加载成功】宽高:" + bitmap.getWidth() + "x" + bitmap.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底:无默认图片时,用代码生成透明/纯色背景(避免报错)
|
||||
*/
|
||||
private void setDefaultBackground() {
|
||||
ivBackground.setScaleType(ScaleType.CENTER_CROP);
|
||||
// 方案1:生成透明背景(推荐,避免白色)
|
||||
ivBackground.setBackgroundColor(getResources().getColor(android.R.color.transparent));
|
||||
// 或 方案2:生成白色背景(根据界面需求选择)
|
||||
// ivBackground.setBackgroundColor(getResources().getColor(android.R.color.white));
|
||||
// 或 方案3:生成灰色背景(更友好,避免纯白/纯黑突兀)
|
||||
// ivBackground.setBackgroundColor(getResources().getColor(android.R.color.darker_gray));
|
||||
|
||||
// 可选:设置一个空的Bitmap,避免ImageView显示空白
|
||||
Bitmap emptyBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
ivBackground.setImageBitmap(emptyBitmap);
|
||||
LogUtils.d(TAG, "【默认背景】使用代码生成透明背景(无图片资源)");
|
||||
}
|
||||
|
||||
// 复用原有尺寸调整方法(若项目中已存在,无需重复添加)
|
||||
private void adjustImageViewSize(Bitmap bitmap) {
|
||||
// 此处保留项目中原有逻辑(如根据控件尺寸缩放图片等)
|
||||
// 示例逻辑(仅供参考):
|
||||
int viewWidth = getWidth();
|
||||
int viewHeight = getHeight();
|
||||
if (viewWidth > 0 && viewHeight > 0 && bitmap != null) {
|
||||
// 计算缩放比例,适配控件尺寸
|
||||
float scale = Math.min((float) viewWidth / bitmap.getWidth(), (float) viewHeight / bitmap.getHeight());
|
||||
// 后续尺寸调整逻辑...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 【对外提供】预览指定路径的临时图片(直接传入路径,不依赖Bean)
|
||||
* 用于:临时预览本地图片、测试图片等场景
|
||||
@@ -198,6 +300,22 @@ public class BackgroundView extends RelativeLayout {
|
||||
LogUtils.d(TAG, "图片路径:" + imagePath + ",宽高比:" + imageAspectRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增工具函数】校验图片路径有效性(路径非空+文件存在+是文件+大小合理)
|
||||
* 用于:reloadPreviewBackground中压缩图/原始图的前置校验,避免无效加载
|
||||
*/
|
||||
private boolean isImagePathValid(String imagePath) {
|
||||
if (imagePath == null || imagePath.isEmpty()) {
|
||||
LogUtils.d(TAG, "图片路径为空,无效");
|
||||
return false;
|
||||
}
|
||||
File imageFile = new File(imagePath);
|
||||
// 校验:文件存在 + 是普通文件 + 大小>100字节(避免空文件/损坏文件)
|
||||
boolean isValid = imageFile.exists() && imageFile.isFile() && imageFile.length() > 100;
|
||||
LogUtils.d(TAG, "图片路径校验:" + imagePath + ",是否有效:" + isValid + "(大小:" + imageFile.length() + "字节)");
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算图片原始宽高比(宽/高)→ 控制不拉伸的核心
|
||||
* @param file 图片文件(非空)
|
||||
|
||||
@@ -31,4 +31,15 @@
|
||||
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
|
||||
<string name="subtitle_activity_about">About The APP</string>
|
||||
<string name="msg_AOHPCTCSeekBar_ClearRecord">>>>Seek 100% Right Is Clean Record.>>></string>
|
||||
|
||||
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
|
||||
<string name="permission_title">权限申请</string>
|
||||
<string name="permission_denied_title">权限被拒绝</string>
|
||||
<string name="permission_grant_success">权限获取成功,请重新操作</string>
|
||||
<string name="permission_storage_rationale">需要存储权限才能选择/拍照/裁剪图片,请授予权限</string>
|
||||
<string name="permission_storage_setting_guide">存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限</string>
|
||||
<string name="confirm">确定</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="go_to_setting">去设置</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user