diff --git a/powerbell/build.properties b/powerbell/build.properties index a74a359f..356beba8 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Dec 03 21:14:37 GMT 2025 +#Wed Dec 03 22:24:41 GMT 2025 stageCount=13 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=180 +buildCount=188 baseBetaVersion=15.11.13 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java index 2f8eae2b..f471e4bb 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -1,25 +1,26 @@ package cc.winboll.studio.powerbell.activities; +import android.app.Activity; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; -import android.provider.Settings; -import android.text.TextUtils; +import android.util.Log; import android.view.View; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; @@ -27,36 +28,26 @@ import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; +import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog; import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.PermissionUtils; -import cc.winboll.studio.powerbell.utils.UriUtil; import cc.winboll.studio.powerbell.views.BackgroundView; 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; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import android.app.Activity; -import android.os.Environment; -import cc.winboll.studio.powerbell.utils.FileUtils; -import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog; /** - * 背景设置Activity(图片选择/拍照/裁剪/预览/保存核心页面) + * 背景设置Activity(图片选择/拍照/裁剪/预览/保存核心页面,Java7兼容,修复裁剪拉伸问题) * 核心特性: * 1. 适配多包名场景(依赖工具类动态获取包名/Authority) * 2. 兼容Android 6.0~14+(权限/存储/裁剪适配) - * 3. 采用工具类解耦(文件管理/权限管理分离,可维护性强) - * 4. 内存优化(Bitmap采样/缩放/回收,避免OOM) + * 3. 修复裁剪拉伸:裁剪后延迟加载图片,确保BackgroundView获取最新比例 + * 4. 优化裁剪流程:启动裁剪时直接使用原图,不额外复制到BackgroundSource目录 * 5. 适配MIUI等特殊机型(裁剪权限/渲染延迟适配) - * 6. 优化裁剪流程:启动裁剪时直接使用原图,不额外复制到BackgroundSource目录 - * 7. 修复选图路径问题:独立管理选图临时文件,双路径强绑定,全链路校验 */ public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { @@ -78,6 +69,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg private File mfTakePhoto; // 配置标记(是否提交设置) boolean isCommitSettings = false; + + int mCropAspectX = 0; + int mCropAspectY = 0; @Override public Activity getActivity() { @@ -95,7 +89,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg setContentView(R.layout.activity_background_settings); // 初始化核心控件 - mBackgroundView = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); + mBackgroundView = (BackgroundView) findViewById(R.id.background_view); // 初始化工具类(单例模式,全局复用,解耦业务逻辑) mBgSourceUtils = BackgroundSourceUtils.getInstance(this); mPermissionUtils = PermissionUtils.getInstance(); @@ -180,14 +174,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - boolean isImageType(String lowerMimeType) { - return lowerMimeType.equals("image/jpeg") + boolean isImageType(String lowerMimeType) { + return lowerMimeType.equals("image/jpeg") || lowerMimeType.equals("image/png") // 新增小众格式适配 || lowerMimeType.equals("image/tiff") || lowerMimeType.equals("image/jpg") // 兼容部分应用传入的"image/jpg"(标准为"image/jpeg") || lowerMimeType.equals("image/svg+xml"); // 若需支持SVG格式,可添加(需确保App支持SVG解析) - } + } @Override public void onAcceptRecivedPicture(String szPreRecivedPictureName) { @@ -206,7 +200,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 1. 修改正式Bean BackgroundBean previewBackgroundBean = mBgSourceUtils.getPreviewBackgroundBean(); previewBackgroundBean.setIsUseBackgroundFile(false); - doubleRefreshPreview(); + doubleRefreshPreview(); } }; @@ -265,22 +259,22 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg public void run() { ToastUtils.show("未找到相册应用,请安装后重试"); new AlertDialog.Builder(BackgroundSettingsActivity.this) - .setTitle("无图片选择应用") - .setMessage("需要安装相册应用才能选择图片") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent marketIntent = new Intent(Intent.ACTION_VIEW); - marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); - if (marketIntent.resolveActivity(getPackageManager()) != null) { - startActivity(marketIntent); - } else { - ToastUtils.show("无法打开应用商店"); - } - } - }) - .setNegativeButton("取消", null) - .show(); + .setTitle("无图片选择应用") + .setMessage("需要安装相册应用才能选择图片") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent marketIntent = new Intent(Intent.ACTION_VIEW); + marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); + if (marketIntent.resolveActivity(getPackageManager()) != null) { + startActivity(marketIntent); + } else { + ToastUtils.show("无法打开应用商店"); + } + } + }) + .setNegativeButton("取消", null) + .show(); } }); } @@ -297,7 +291,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】触发固定比例裁剪功能"); - startCropImageActivity(false); + startCropImageActivity(false); } }; @@ -308,7 +302,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】触发自由裁剪功能"); - startCropImageActivity(true); + startCropImageActivity(true); } }; @@ -404,7 +398,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg int oldColor = bean.getPixelColor(); bean.setPixelColor(0); mBgSourceUtils.saveSettings(); - doubleRefreshPreview(); + doubleRefreshPreview(); ToastUtils.show("像素颜色已清空"); LogUtils.d(TAG, "【像素清空】操作完成:旧颜色值=" + oldColor + ",新颜色值=0,配置已保存"); } @@ -414,228 +408,225 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * 启动图片裁剪活动(核心优化:直接使用预览图原图,不复制到BackgroundSource) * @param isCropFree 是否自由裁剪 */ - public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseBackgroundScaledCompressFile(false); - mBgSourceUtils.saveSettings(); - doubleRefreshPreview(); +// public void startCropImageActivity(boolean isCropFree) { +// LogUtils.d(TAG, "【裁剪启动】startCropImageActivity 触发,自由裁剪:" + isCropFree); +// BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); +// previewBean.setIsUseBackgroundScaledCompressFile(false); +// mBgSourceUtils.saveSettings(); +// doubleRefreshPreview(); +// +// File previewFile = new File(previewBean.getBackgroundFilePath()); // 裁剪缓存图片 +// +// // 2. 生成裁剪输入Uri(强化MIUI权限授予,直接用原图Uri) +// Uri inputUri = null; +// try { +// inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile); +// // 显式授予MIUI裁剪工具读写权限(关键适配) +// grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); +// LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString() + ",已授予MIUI裁剪工具权限"); +// } catch (Exception e) { +// LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败:" + e.getMessage(), e); +// ToastUtils.show("图片裁剪失败:无法获取图片权限"); +// return; +// } +// +// // 3. 【核心优化】直接使用预览图原图作为裁剪输入,删除原工具类创建裁剪路径的复制逻辑 +// // 仅创建裁剪结果临时文件(用于接收裁剪输出,不涉及原图复制) +// File cropResultTempFile = new File(previewBean.getBackgroundScaledCompressFilePath()); +// if (cropResultTempFile == null) { +// ToastUtils.show("裁剪路径创建失败,请重试"); +// return; +// } +// +// // 4. 构建裁剪意图(适配MIUI核心修改,输入为原图Uri) +// Intent intent = new Intent("com.android.camera.action.CROP"); +// intent.setDataAndType(inputUri, "image/*"); +// intent.putExtra("crop", "true"); +// intent.putExtra("noFaceDetection", true); +// +// // 裁剪比例设置 +// if (!isCropFree) { +// int viewWidth = mBackgroundView.getWidth() > 0 ? mBackgroundView.getWidth() : getResources().getDisplayMetrics().widthPixels; +// int viewHeight = mBackgroundView.getHeight() > 0 ? mBackgroundView.getHeight() : getResources().getDisplayMetrics().heightPixels; +// int gcd = calculateGCD(viewWidth, viewHeight); +// intent.putExtra("aspectX", viewWidth / gcd); +// intent.putExtra("aspectY", viewHeight / gcd); +// } else { +// // 真正自由裁剪:移除aspectX/aspectY参数(不设置则裁剪窗口可自由调整比例) +// LogUtils.d(TAG, "【裁剪比例】启用真正自由裁剪(移除固定比例限制)"); +// } +// +// // 输出尺寸设置(统一用2048,适配所有版本) +// int maxOutputSize = 2048; +// int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); +// int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); +// intent.putExtra("outputX", outputX); +// intent.putExtra("outputY", outputY); +// intent.putExtra("scale", true); +// intent.putExtra("scaleUpIfNeeded", true); +// +// // 输出配置(指定JPEG格式,适配MIUI) +// intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); +// intent.putExtra("quality", 80); +// intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM +// +// // 权限Flags(添加持久化权限) +// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); +// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { +// intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); +// } +// +// // 5. 适配系统裁剪工具(MIUI专属处理) +// try { +// List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); +// if (!resolveInfos.isEmpty()) { +// ResolveInfo resolveInfo = resolveInfos.get(0); +// String cropPackageName = resolveInfo.activityInfo.packageName; +// String cropActivityName = resolveInfo.activityInfo.name; +// LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + ",Activity=" + cropActivityName); +// +// // 生成输出Uri(显式授予MIUI写入权限,输出到临时结果文件) +// Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropResultTempFile); +// // 适配MIUI裁剪工具,额外授予持久化写入权限 +// if (cropPackageName.equals("com.miui.gallery")) { +// grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); +// LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限"); +// } +// +// // 显式设置Component +// Intent cropIntent = new Intent(intent); +// cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); +// cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); +// cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); +// +// // 保存裁剪结果临时文件路径到工具类(用于回调时获取) +// mBgSourceUtils.getPreviewBackgroundBean().setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); +// startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); +// LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropResultTempFile.getAbsolutePath()); +// } else { +// // 兜底:启动第三方裁剪工具 +// Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropResultTempFile); +// intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); +// Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); +// chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); +// +// // 保存裁剪结果临时文件路径到工具类 +// mBgSourceUtils.getPreviewBackgroundBean().setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); +// if (chooser.resolveActivity(getPackageManager()) != null) { +// startActivityForResult(chooser, REQUEST_CROP_IMAGE); +// } else { +// ToastUtils.show("无可用裁剪工具,请安装系统相机"); +// } +// } +// } catch (Exception e) { +// LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e); +// ToastUtils.show("无法启动裁剪工具"); +// } +// } - File previewFile = new File(previewBean.getBackgroundFilePath()); // 裁剪缓存图片 - - // 2. 生成裁剪输入Uri(强化MIUI权限授予,直接用原图Uri) - Uri inputUri = null; - try { - inputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), previewFile); - // 显式授予MIUI裁剪工具读写权限(关键适配) - grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - LogUtils.d(TAG, "【裁剪Uri】输入Uri生成成功 : " + inputUri.toString() + ",已授予MIUI裁剪工具权限"); - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】生成输入Uri失败:" + e.getMessage(), e); - ToastUtils.show("图片裁剪失败:无法获取图片权限"); - return; + /** + * 计算两个整数的最大公约数(GCD,Greatest Common Divisor) + * 用途:裁剪时获取视图宽高的最简比例(避免比例值过大导致裁剪工具崩溃) + * 算法:欧几里得算法(高效稳定,时间复杂度O(log(min(a,b))),兼容Java7) + * @param a 整数1(如视图宽度) + * @param b 整数2(如视图高度) + * @return 最大公约数(最小返回1,避免比例计算时分母为0) + */ + private int calculateGCD(int a, int b) { + // 边界值校验(避免传入0导致死循环,适配视图宽高为0的极端情况) + if (a <= 0 || b <= 0) { + LogUtils.w(TAG, "【GCD计算】输入值无效(a=" + a + ", b=" + b + "),返回默认值1"); + return 1; } - // 3. 【核心优化】直接使用预览图原图作为裁剪输入,删除原工具类创建裁剪路径的复制逻辑 - // 仅创建裁剪结果临时文件(用于接收裁剪输出,不涉及原图复制) - File cropResultTempFile = new File(previewBean.getBackgroundScaledCompressFilePath()); - if (cropResultTempFile == null) { - ToastUtils.show("裁剪路径创建失败,请重试"); - return; + // 欧几里得算法核心逻辑(Java7 循环写法,无Java8+特性) + int temp; + while (b != 0) { + temp = a % b; // 取余 + a = b; // 赋值:把b作为新的a + b = temp; // 赋值:把余数作为新的b } - // 4. 构建裁剪意图(适配MIUI核心修改,输入为原图Uri) - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(inputUri, "image/*"); - intent.putExtra("crop", "true"); - intent.putExtra("noFaceDetection", true); - - // 裁剪比例设置 - if (!isCropFree) { - int viewWidth = mBackgroundView.getWidth() > 0 ? mBackgroundView.getWidth() : getResources().getDisplayMetrics().widthPixels; - int viewHeight = mBackgroundView.getHeight() > 0 ? mBackgroundView.getHeight() : getResources().getDisplayMetrics().heightPixels; - int gcd = calculateGCD(viewWidth, viewHeight); - intent.putExtra("aspectX", viewWidth / gcd); - intent.putExtra("aspectY", viewHeight / gcd); - } else { - // 真正自由裁剪:移除aspectX/aspectY参数(不设置则裁剪窗口可自由调整比例) - LogUtils.d(TAG, "【裁剪比例】启用真正自由裁剪(移除固定比例限制)"); - // 注释或删除以下两行代码(不设置aspectX/aspectY,裁剪窗口即可自由调整宽高比) - // intent.putExtra("aspectX", 1); - // intent.putExtra("aspectY", 1); - } - - // 输出尺寸设置(统一用2048,适配所有版本) - int maxOutputSize = 2048; - int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); - int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); - intent.putExtra("outputX", outputX); - intent.putExtra("outputY", outputY); - intent.putExtra("scale", true); - intent.putExtra("scaleUpIfNeeded", true); - - // 输出配置(指定JPEG格式,适配MIUI) - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); - intent.putExtra("quality", 80); - intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM - - // 权限Flags(添加持久化权限) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } - - // 5. 适配系统裁剪工具(MIUI专属处理) - try { - List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - if (!resolveInfos.isEmpty()) { - ResolveInfo resolveInfo = resolveInfos.get(0); - String cropPackageName = resolveInfo.activityInfo.packageName; - String cropActivityName = resolveInfo.activityInfo.name; - LogUtils.d(TAG, "【裁剪适配】找到裁剪工具:包名=" + cropPackageName + ",Activity=" + cropActivityName); - - // 生成输出Uri(显式授予MIUI写入权限,输出到临时结果文件) - Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropResultTempFile); - // 适配MIUI裁剪工具,额外授予持久化写入权限 - if (cropPackageName.equals("com.miui.gallery")) { - grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - LogUtils.d(TAG, "【裁剪适配】已授予MIUI裁剪工具输出Uri写入权限"); - } - - // 显式设置Component - Intent cropIntent = new Intent(intent); - cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); - cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); - cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - - // 保存裁剪结果临时文件路径到工具类(用于回调时获取) - mBgSourceUtils.getPreviewBackgroundBean().setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); - startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); - LogUtils.d(TAG, "【裁剪启动】已启动系统裁剪工具,输出路径:" + cropResultTempFile.getAbsolutePath()); - } else { - // 兜底:启动第三方裁剪工具 - Uri outputUri = FileProvider.getUriForFile(this, mBgSourceUtils.getFileProviderAuthority(), cropResultTempFile); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); - Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); - chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - - // 保存裁剪结果临时文件路径到工具类 - mBgSourceUtils.getPreviewBackgroundBean().setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooser, REQUEST_CROP_IMAGE); - } else { - ToastUtils.show("无可用裁剪工具,请安装系统相机"); - } - } - } catch (Exception e) { - LogUtils.e(TAG, "【裁剪异常】启动裁剪工具失败:" + e.getMessage(), e); - ToastUtils.show("无法启动裁剪工具"); - } + // 最终a即为最大公约数(确保返回值≥1,避免比例计算异常) + int gcdResult = Math.max(a, 1); + LogUtils.d(TAG, "【GCD计算】完成,输入(宽=" + (a + b) + ", 高=" + (a) + "),最大公约数:" + gcdResult); + return gcdResult; } - /** - * 计算两个整数的最大公约数(GCD,Greatest Common Divisor) - * 用途:裁剪时获取视图宽高的最简比例(避免比例值过大导致裁剪工具崩溃) - * 算法:欧几里得算法(高效稳定,时间复杂度O(log(min(a,b))),兼容Java7) - * @param a 整数1(如视图宽度) - * @param b 整数2(如视图高度) - * @return 最大公约数(最小返回1,避免比例计算时分母为0) - */ - private int calculateGCD(int a, int b) { - // 边界值校验(避免传入0导致死循环,适配视图宽高为0的极端情况) - if (a <= 0 || b <= 0) { - LogUtils.w(TAG, "【GCD计算】输入值无效(a=" + a + ", b=" + b + "),返回默认值1"); - return 1; - } + /** + * 图片缩放(核心用途:大图片自动缩小,降低内存占用,避免OOM崩溃) + * 逻辑:按指定缩放比例(0~1之间)缩小Bitmap,保持宽高比不变,生成新的Bitmap并回收旧图 + * @param originalBitmap 原始Bitmap(需确保非null、未回收) + * @param scale 缩放比例(0 < scale ≤ 1,如0.8表示缩小到原尺寸的80%) + * @return 缩放后的新Bitmap(若缩放失败返回原始Bitmap,避免空指针) + */ + private Bitmap scaleBitmap(Bitmap originalBitmap, float scale) { + // 边界值校验(规避异常场景) + if (originalBitmap == null || originalBitmap.isRecycled()) { + LogUtils.e(TAG, "【图片缩放】原始Bitmap为空或已回收,缩放失败"); + return originalBitmap; // 返回原始图,避免后续空指针 + } + if (scale <= 0 || scale > 1) { + LogUtils.w(TAG, "【图片缩放】缩放比例无效(scale=" + scale + "),需传入0~1之间的值,直接返回原始图"); + return originalBitmap; + } + if (scale == 1.0f) { + LogUtils.d(TAG, "【图片缩放】缩放比例为1.0,无需缩放,直接返回原始图"); + return originalBitmap; + } - // 欧几里得算法核心逻辑(Java7 循环写法,无Java8+特性) - int temp; - while (b != 0) { - temp = a % b; // 取余 - a = b; // 赋值:把b作为新的a - b = temp; // 赋值:把余数作为新的b - } + // Java7 语法实现:按比例计算新宽高(保持宽高比) + int originalWidth = originalBitmap.getWidth(); + int originalHeight = originalBitmap.getHeight(); + int newWidth = Math.round(originalWidth * scale); // 四舍五入取整,避免尺寸异常 + int newHeight = Math.round(originalHeight * scale); - // 最终a即为最大公约数(确保返回值≥1,避免比例计算异常) - int gcdResult = Math.max(a, 1); - LogUtils.d(TAG, "【GCD计算】完成,输入(宽=" + (a + b) + ", 高=" + (a) + "),最大公约数:" + gcdResult); - return gcdResult; - } + LogUtils.d(TAG, "【图片缩放】开始缩放:"); + LogUtils.d(TAG, "→ 原始尺寸:" + originalWidth + "x" + originalHeight); + LogUtils.d(TAG, "→ 缩放比例:" + scale); + LogUtils.d(TAG, "→ 目标尺寸:" + newWidth + "x" + newHeight); - /** - * 图片缩放(核心用途:大图片自动缩小,降低内存占用,避免OOM崩溃) - * 逻辑:按指定缩放比例(0~1之间)缩小Bitmap,保持宽高比不变,生成新的Bitmap并回收旧图 - * @param originalBitmap 原始Bitmap(需确保非null、未回收) - * @param scale 缩放比例(0 < scale ≤ 1,如0.8表示缩小到原尺寸的80%) - * @return 缩放后的新Bitmap(若缩放失败返回原始Bitmap,避免空指针) - */ - private Bitmap scaleBitmap(Bitmap originalBitmap, float scale) { - // 边界值校验(规避异常场景) - if (originalBitmap == null || originalBitmap.isRecycled()) { - LogUtils.e(TAG, "【图片缩放】原始Bitmap为空或已回收,缩放失败"); - return originalBitmap; // 返回原始图,避免后续空指针 - } - if (scale <= 0 || scale > 1) { - LogUtils.w(TAG, "【图片缩放】缩放比例无效(scale=" + scale + "),需传入0~1之间的值,直接返回原始图"); - return originalBitmap; - } - if (scale == 1.0f) { - LogUtils.d(TAG, "【图片缩放】缩放比例为1.0,无需缩放,直接返回原始图"); - return originalBitmap; - } + // 核心缩放逻辑(使用Matrix矩阵缩放,Android原生API,兼容Java7,性能稳定) + Bitmap scaledBitmap = null; + Matrix matrix = new Matrix(); + matrix.setScale(scale, scale); // 设置缩放比例(x/y轴统一缩放,保持宽高比) - // Java7 语法实现:按比例计算新宽高(保持宽高比) - int originalWidth = originalBitmap.getWidth(); - int originalHeight = originalBitmap.getHeight(); - int newWidth = Math.round(originalWidth * scale); // 四舍五入取整,避免尺寸异常 - int newHeight = Math.round(originalHeight * scale); - - LogUtils.d(TAG, "【图片缩放】开始缩放:"); - LogUtils.d(TAG, "→ 原始尺寸:" + originalWidth + "x" + originalHeight); - LogUtils.d(TAG, "→ 缩放比例:" + scale); - LogUtils.d(TAG, "→ 目标尺寸:" + newWidth + "x" + newHeight); - - // 核心缩放逻辑(使用Matrix矩阵缩放,Android原生API,兼容Java7,性能稳定) - Bitmap scaledBitmap = null; - Matrix matrix = new Matrix(); - matrix.setScale(scale, scale); // 设置缩放比例(x/y轴统一缩放,保持宽高比) - - try { - // 生成缩放后的Bitmap(过滤无效尺寸,避免生成0x0的空图) - if (newWidth > 0 && newHeight > 0) { - scaledBitmap = Bitmap.createBitmap( + try { + // 生成缩放后的Bitmap(过滤无效尺寸,避免生成0x0的空图) + if (newWidth > 0 && newHeight > 0) { + scaledBitmap = Bitmap.createBitmap( originalBitmap, 0, 0, // 裁剪起始坐标(0,0表示完整缩放,不裁剪) originalWidth, originalHeight, // 原始图宽高 matrix, // 缩放矩阵 true // 开启抗锯齿,提升缩放后图片清晰度 - ); - LogUtils.d(TAG, "【图片缩放】缩放成功,新图内存大小:" + scaledBitmap.getByteCount() / 1024 + "KB"); - } else { - LogUtils.e(TAG, "【图片缩放】目标尺寸无效(" + newWidth + "x" + newHeight + "),缩放失败"); - scaledBitmap = originalBitmap; // 兜底返回原始图 - } + ); + LogUtils.d(TAG, "【图片缩放】缩放成功,新图内存大小:" + scaledBitmap.getByteCount() / 1024 + "KB"); + } else { + LogUtils.e(TAG, "【图片缩放】目标尺寸无效(" + newWidth + "x" + newHeight + "),缩放失败"); + scaledBitmap = originalBitmap; // 兜底返回原始图 + } - } catch (OutOfMemoryError e) { - // 捕获OOM异常(极端情况下缩放仍可能OOM,避免崩溃) - LogUtils.d(TAG, "【图片缩放】OOM异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); - scaledBitmap = originalBitmap; // 兜底返回原始图 - ToastUtils.show("图片缩放失败,可能因图片过大"); - } catch (Exception e) { - // 捕获其他异常(如Bitmap状态异常) - LogUtils.e(TAG, "【图片缩放】未知异常:" + e.getMessage(), e); - scaledBitmap = originalBitmap; // 兜底返回原始图 - } + } catch (OutOfMemoryError e) { + // 捕获OOM异常(极端情况下缩放仍可能OOM,避免崩溃) + LogUtils.d(TAG, "【图片缩放】OOM异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); + scaledBitmap = originalBitmap; // 兜底返回原始图 + ToastUtils.show("图片缩放失败,可能因图片过大"); + } catch (Exception e) { + // 捕获其他异常(如Bitmap状态异常) + LogUtils.e(TAG, "【图片缩放】未知异常:" + e.getMessage(), e); + scaledBitmap = originalBitmap; // 兜底返回原始图 + } - // 回收原始Bitmap(仅当新图生成成功且与原图不同时,避免重复回收) - if (scaledBitmap != null && scaledBitmap != originalBitmap && !originalBitmap.isRecycled()) { - originalBitmap.recycle(); - LogUtils.d(TAG, "【图片缩放】已回收原始Bitmap,释放内存"); - } + // 回收原始Bitmap(仅当新图生成成功且与原图不同时,避免重复回收) + if (scaledBitmap != null && scaledBitmap != originalBitmap && !originalBitmap.isRecycled()) { + originalBitmap.recycle(); + LogUtils.d(TAG, "【图片缩放】已回收原始Bitmap,释放内存"); + } - return scaledBitmap; - } + return scaledBitmap; + } // ======================================== Activity回调处理(选图/拍照/裁剪) ======================================== @Override @@ -666,7 +657,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg handleTakePhotoResult(resultCode, data); // 拍照结果处理 break; case REQUEST_CROP_IMAGE: - handleCropImageResult(requestCode, resultCode, data); // 裁剪结果处理 + handleCropImageResult(requestCode, resultCode, data); // 裁剪结果处理(修复拉伸核心) break; default: LogUtils.d(TAG, "【回调忽略】未知 requestCode:" + requestCode + ",无需处理"); @@ -675,8 +666,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } catch (Exception e) { LogUtils.e(TAG, "【回调异常】onActivityResult 全局异常:" + e.getMessage(), e); ToastUtils.show("操作失败,请重试"); - //mBgSourceUtils.clearCropTempFiles(); // 异常时清理临时文件,避免残留 - //clearSelectTempFile(); // 新增:异常时清理选图临时文件 } } @@ -705,7 +694,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 校验拍照文件有效性 if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { ToastUtils.show("拍照文件不存在或损坏"); - //mBgSourceUtils.clearCropTempFiles(); return; } @@ -715,92 +703,90 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap); } else { ToastUtils.show("拍照图片为空"); - //mBgSourceUtils.clearCropTempFiles(); return; } // 同步预览并启动裁剪 mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath()); - doubleRefreshPreview(); + doubleRefreshPreview(); - startCropImageActivity(false); + startCropImageActivity(false); LogUtils.d(TAG, "【拍照完成】拍照回调处理结束,已启动裁剪"); } - /** - * 解析拍照结果为Bitmap(核心用途:拍照回调后,从拍照临时文件/Intent中获取Bitmap,用于后续压缩保存) - * 逻辑:优先从拍照临时文件解析(清晰原图),失败则从Intent获取缩略图(兜底),双重保障避免解析失败 - * @param data 拍照回调的Intent(可能包含缩略图,部分机型不返回) - * @return 解析后的Bitmap(解析失败返回null,避免空指针传播) - */ - private Bitmap getTakePhotoBitmap(Intent data) { - LogUtils.d(TAG, "【拍照Bitmap解析】开始解析拍照结果"); + /** + * 解析拍照结果为Bitmap(核心用途:拍照回调后,从拍照临时文件/Intent中获取Bitmap,用于后续压缩保存) + * 逻辑:优先从拍照临时文件解析(清晰原图),失败则从Intent获取缩略图(兜底),双重保障避免解析失败 + * @param data 拍照回调的Intent(可能包含缩略图,部分机型不返回) + * @return 解析后的Bitmap(解析失败返回null,避免空指针传播) + */ + private Bitmap getTakePhotoBitmap(Intent data) { + LogUtils.d(TAG, "【拍照Bitmap解析】开始解析拍照结果"); - // 方案1:优先从拍照临时文件解析(推荐,获取清晰原图,避免缩略图模糊) - if (mfTakePhoto != null && mfTakePhoto.exists()) { - LogUtils.d(TAG, "【拍照Bitmap解析】优先从临时文件解析:" + mfTakePhoto.getAbsolutePath()); - // 复用已定义的parseCropTempFileToBitmap函数(采样率加载,防OOM,无需重复写逻辑) - Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto); - if (photoBitmap != null && !photoBitmap.isRecycled()) { - LogUtils.d(TAG, "【拍照Bitmap解析】从临时文件解析成功"); - return photoBitmap; - } else { - LogUtils.w(TAG, "【拍照Bitmap解析】从临时文件解析失败,尝试从Intent获取缩略图兜底"); - } - } else { - LogUtils.w(TAG, "【拍照Bitmap解析】拍照临时文件无效,尝试从Intent获取缩略图兜底"); - } + // 方案1:优先从拍照临时文件解析(推荐,获取清晰原图,避免缩略图模糊) + if (mfTakePhoto != null && mfTakePhoto.exists()) { + LogUtils.d(TAG, "【拍照Bitmap解析】优先从临时文件解析:" + mfTakePhoto.getAbsolutePath()); + // 复用已定义的parseCropTempFileToBitmap函数(采样率加载,防OOM,无需重复写逻辑) + Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto); + if (photoBitmap != null && !photoBitmap.isRecycled()) { + LogUtils.d(TAG, "【拍照Bitmap解析】从临时文件解析成功"); + return photoBitmap; + } else { + LogUtils.w(TAG, "【拍照Bitmap解析】从临时文件解析失败,尝试从Intent获取缩略图兜底"); + } + } else { + LogUtils.w(TAG, "【拍照Bitmap解析】拍照临时文件无效,尝试从Intent获取缩略图兜底"); + } - // 方案2:从Intent获取缩略图(兜底方案,适配部分机型拍照后不写入临时文件的场景) - if (data != null) { - try { - // 从Intent中获取缩略图(部分机型仅返回此缩略图,清晰度较低) - Bitmap thumbnailBitmap = (Bitmap) data.getParcelableExtra("data"); - if (thumbnailBitmap != null && !thumbnailBitmap.isRecycled()) { - LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取缩略图成功,尺寸:" + thumbnailBitmap.getWidth() + "x" + thumbnailBitmap.getHeight()); - return thumbnailBitmap; - } else { - LogUtils.e(TAG, "【拍照Bitmap解析】从Intent获取缩略图失败(Bitmap为空或已回收)"); - } - } catch (Exception e) { - // 捕获异常(如Intent数据格式异常、Parcelable解析失败等) - LogUtils.e(TAG, "【拍照Bitmap解析】从Intent解析缩略图异常:" + e.getMessage(), e); - } - } else { - LogUtils.e(TAG, "【拍照Bitmap解析】拍照回调Intent为空,无法获取缩略图"); - } + // 方案2:从Intent获取缩略图(兜底方案,适配部分机型拍照后不写入临时文件的场景) + if (data != null) { + try { + // 从Intent中获取缩略图(部分机型仅返回此缩略图,清晰度较低) + Bitmap thumbnailBitmap = (Bitmap) data.getParcelableExtra("data"); + if (thumbnailBitmap != null && !thumbnailBitmap.isRecycled()) { + LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取缩略图成功,尺寸:" + thumbnailBitmap.getWidth() + "x" + thumbnailBitmap.getHeight()); + return thumbnailBitmap; + } else { + LogUtils.e(TAG, "【拍照Bitmap解析】从Intent获取缩略图失败(Bitmap为空或已回收)"); + } + } catch (Exception e) { + // 捕获异常(如Intent数据格式异常、Parcelable解析失败等) + LogUtils.e(TAG, "【拍照Bitmap解析】从Intent解析缩略图异常:" + e.getMessage(), e); + } + } else { + LogUtils.e(TAG, "【拍照Bitmap解析】拍照回调Intent为空,无法获取缩略图"); + } - // 所有方案均失败 - LogUtils.e(TAG, "【拍照Bitmap解析】解析失败(临时文件+Intent均无有效Bitmap)"); - ToastUtils.show("拍照图片解析失败,请重试"); - return null; - } + // 所有方案均失败 + LogUtils.e(TAG, "【拍照Bitmap解析】解析失败(临时文件+Intent均无有效Bitmap)"); + ToastUtils.show("拍照图片解析失败,请重试"); + return null; + } /** * 处理裁剪回调(调用工具类获取裁剪文件) */ - private void handleCropImageResult(int requestCode, int resultCode, Intent data) { - LogUtils.d(TAG, "handleCropImageResult"); - // 从工具类获取裁剪临时文件(唯一入口) - File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath()); - boolean isFileExist = cropTempFile.exists(); - boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; - long fileSize = isFileExist ? cropTempFile.length() : 0; - // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功 - boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; - if (isCropSuccess) { - LogUtils.d(TAG, "isCropSuccess == true"); - BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); - previewBean.setIsUseBackgroundFile(true); - previewBean.setIsUseBackgroundScaledCompressFile(true); - mBgSourceUtils.saveSettings(); - mBackgroundView.loadBackgroundBean(previewBean); - LogUtils.d(TAG, "isCropSuccess == true && mBackgroundView.loadBackground"); - } else { - // 其他失败场景 - handleOperationCancelOrFail(); - } - } + + + /** + * 处理裁剪回调(核心修改:按裁剪比例强制调整保存图片比例) + */ + + + /** + * 核心新增方法:按裁剪比例强制调整Bitmap尺寸(确保保存比例与裁剪比例一致) + * @param originalBitmap 裁剪后的原始Bitmap + * @param cropAspectRatio 裁剪时设置的宽高比(宽/高) + * @return 按比例调整后的Bitmap + */ + + + /** + * 核心新增方法:将调整比例后的Bitmap保存到文件(覆盖原裁剪文件,确保保存比例正确) + * @param bitmap 按比例调整后的Bitmap + * @param targetFile 目标文件(原裁剪文件路径) + */ + /** * 解析裁剪临时文件为Bitmap(核心用途:裁剪回调后,将裁剪生成的临时文件转为Bitmap,用于后续保存/预览) @@ -808,77 +794,85 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * @param cropTempFile 裁剪后的临时文件(来自工具类getCropTempFile()) * @return 解析后的Bitmap(解析失败返回null,避免空指针传播) */ - private Bitmap parseCropTempFileToBitmap(File cropTempFile) { - // 第一重校验:文件对象有效性(规避空指针/非文件) - if (cropTempFile == null) { - LogUtils.e(TAG, "【裁剪文件解析】裁剪临时文件对象为空,解析失败"); - return null; - } - if (!cropTempFile.exists()) { - LogUtils.e(TAG, "【裁剪文件解析】裁剪临时文件不存在,路径:" + cropTempFile.getAbsolutePath()); - return null; - } - if (!cropTempFile.isFile()) { - LogUtils.e(TAG, "【裁剪文件解析】路径不是合法文件,路径:" + cropTempFile.getAbsolutePath()); - return null; - } - if (cropTempFile.length() <= 100) { // 过滤小于100字节的空文件/破损文件 - LogUtils.e(TAG, "【裁剪文件解析】裁剪文件过小(" + cropTempFile.length() + "bytes),视为无效文件"); - return null; - } + + + /** + * 处理裁剪回调(核心修改:按裁剪比例强制调整保存图片比例) + */ + - LogUtils.d(TAG, "【裁剪文件解析】开始解析裁剪文件:" + cropTempFile.getAbsolutePath() + ",大小:" + cropTempFile.length() + "bytes"); + /** + * 核心新增方法:按裁剪比例强制调整Bitmap尺寸(确保保存比例与裁剪比例一致) + * @param originalBitmap 裁剪后的原始Bitmap + * @param cropAspectRatio 裁剪时设置的宽高比(宽/高) + * @return 按比例调整后的Bitmap + */ + private Bitmap adjustBitmapToCropRatio(Bitmap originalBitmap, float cropAspectRatio) { + LogUtils.d(TAG, "【比例调整】开始按裁剪比例调整Bitmap,目标比例:" + cropAspectRatio); + if (originalBitmap == null || originalBitmap.isRecycled() || cropAspectRatio <= 0) { + LogUtils.e(TAG, "【比例调整】参数无效,直接返回原Bitmap"); + return originalBitmap; + } - // Java7 语法:Bitmap采样率加载(核心优化,避免OOM) - BitmapFactory.Options options = new BitmapFactory.Options(); - // 第一步:仅获取图片尺寸,不加载完整Bitmap到内存(避免大文件直接加载OOM) - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + int originalWidth = originalBitmap.getWidth(); + int originalHeight = originalBitmap.getHeight(); + float originalRatio = (float) originalWidth / originalHeight; - // 第二步:计算采样率(最大尺寸限制为2048px,适配所有机型屏幕,平衡清晰度与内存) - int maxSize = 2048; - int sampleSize = 1; // 采样率(1表示不采样,2表示缩小为1/2,4表示1/4,以此类推) - // Java7 普通while循环计算采样率 - while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) { - sampleSize *= 2; // 每次翻倍,高效缩小(避免暴力枚举) - } - // 限制采样率最大为16(避免缩放过小导致图片模糊) - sampleSize = Math.min(sampleSize, 16); - LogUtils.d(TAG, "【裁剪文件解析】图片原始尺寸:" + options.outWidth + "x" + options.outHeight + ",采样率:" + sampleSize); + // 若原比例与裁剪比例一致,无需调整 + if (Math.abs(originalRatio - cropAspectRatio) < 0.01f) { + LogUtils.d(TAG, "【比例调整】原比例与裁剪比例一致,无需调整"); + return originalBitmap; + } - // 第三步:设置采样率,加载完整Bitmap(优化内存配置) - options.inJustDecodeBounds = false; - options.inSampleSize = sampleSize; - options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存(RGB_565仅占ARGB_8888的一半内存) - options.inPurgeable = true; // 允许内存不足时回收Bitmap(Android低版本适配) - options.inInputShareable = true; // 配合inPurgeable使用,提升内存回收效率 + // 按裁剪比例计算目标尺寸(优先匹配原Bitmap的宽/高中较小的维度,避免拉伸) + int targetWidth, targetHeight; + if (originalWidth < originalHeight) { + // 原图宽度更小 → 按宽度适配,计算目标高度 + targetWidth = originalWidth; + targetHeight = Math.round(targetWidth / cropAspectRatio); + } else { + // 原图高度更小 → 按高度适配,计算目标宽度 + targetHeight = originalHeight; + targetWidth = Math.round(targetHeight * cropAspectRatio); + } - // 第四步:解析文件为Bitmap(捕获异常,避免崩溃) - Bitmap cropBitmap = null; - try { - cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); - if (cropBitmap == null) { - LogUtils.e(TAG, "【裁剪文件解析】decodeFile返回null,文件可能破损或格式不支持"); - return null; - } - if (cropBitmap.isRecycled()) { - LogUtils.e(TAG, "【裁剪文件解析】解析出的Bitmap已被回收"); - return null; - } - LogUtils.d(TAG, "【裁剪文件解析】解析成功,Bitmap内存大小:" + cropBitmap.getByteCount() / 1024 + "KB"); - } catch (OutOfMemoryError e) { - // 捕获OOM异常(极端大文件解析时的核心异常) - LogUtils.d(TAG, "【裁剪文件解析】OOM异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); - ToastUtils.show("图片解析失败,可能因图片过大"); - return null; - } catch (Exception e) { - // 捕获其他异常(如文件权限不足、格式不支持等) - LogUtils.e(TAG, "【裁剪文件解析】未知异常:" + e.getMessage(), e); - return null; - } + LogUtils.d(TAG, "【比例调整】调整前尺寸:" + originalWidth + "x" + originalHeight + ",调整后尺寸:" + targetWidth + "x" + targetHeight); - return cropBitmap; - } + // 按目标尺寸裁剪Bitmap(保持比例,切除多余部分,避免拉伸) + Bitmap adjustedBitmap = Bitmap.createBitmap( + originalBitmap, + (originalWidth - targetWidth) / 2, // 水平居中裁剪 + (originalHeight - targetHeight) / 2, // 垂直居中裁剪 + targetWidth, + targetHeight + ); + + LogUtils.d(TAG, "【比例调整】按裁剪比例调整完成,新比例:" + (float) targetWidth / targetHeight); + return adjustedBitmap; + } + + /** + * 核心新增方法:将调整比例后的Bitmap保存到文件(覆盖原裁剪文件,确保保存比例正确) + * @param bitmap 按比例调整后的Bitmap + * @param targetFile 目标文件(原裁剪文件路径) + */ + + + /** + * 解析裁剪临时文件为Bitmap(核心用途:裁剪回调后,将裁剪生成的临时文件转为Bitmap,用于后续保存/预览) + * 逻辑:采用采样率加载(避免OOM),支持JPEG/PNG等主流格式,兼容破损文件/空文件场景 + * @param cropTempFile 裁剪后的临时文件(来自工具类getCropTempFile()) + * @return 解析后的Bitmap(解析失败返回null,避免空指针传播) + */ + + + /** + * 解析裁剪临时文件为Bitmap(核心用途:裁剪回调后,将裁剪生成的临时文件转为Bitmap,用于后续保存/预览) + * 逻辑:采用采样率加载(避免OOM),支持JPEG/PNG等主流格式,兼容破损文件/空文件场景 + * @param cropTempFile 裁剪后的临时文件(来自工具类getCropTempFile()) + * @return 解析后的Bitmap(解析失败返回null,避免空指针传播) + */ + /** * 双重刷新预览(核心用途:适配MIUI等机型裁剪/选图后预览渲染延迟,解决“图片已保存但界面空白”问题) @@ -997,4 +991,627 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }); } } + /** + * 处理裁剪回调(核心修改:完全依赖系统裁剪结果,独立计算比例,不与BackgroundView互传) + */ +// private void handleCropImageResult(int requestCode, int resultCode, Intent data) { +// LogUtils.d(TAG, "handleCropImageResult"); +// // 从工具类获取系统裁剪输出的临时文件(比例由系统裁剪应用决定,仅读取此文件) +// File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath()); +// boolean isFileExist = cropTempFile.exists(); +// boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; +// long fileSize = isFileExist ? cropTempFile.length() : 0; +// // 适配MIUI:仅resultCode=RESULT_OK且文件有效时视为成功 +// boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; +// if (isCropSuccess) { +// LogUtils.d(TAG, "isCropSuccess == true,按系统裁剪文件比例保存"); +// final BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); +// previewBean.setIsUseBackgroundFile(true); +// previewBean.setIsUseBackgroundScaledCompressFile(true); +// mBgSourceUtils.saveSettings(); +// +// // 核心修改1:解析系统裁剪文件,获取系统给出的真实宽高比(独立计算,不依赖BackgroundView) +// float systemCropRatio = getRatioFromSystemCropFile(cropTempFile); +// if (systemCropRatio > 0) { +// // 核心:按系统裁剪比例调整Bitmap并覆盖保存(确保保存比例=系统裁剪比例) +// Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); +// if (cropBitmap != null && !cropBitmap.isRecycled()) { +// // 按系统裁剪比例调整尺寸(仅对齐比例,不改变系统裁剪的主体内容) +// Bitmap scaledCropBitmap = adjustBitmapToSystemCropRatio(cropBitmap, systemCropRatio); +// // 覆盖保存到原裁剪文件(替换系统输出的文件,确保最终保存比例正确) +// saveScaledBitmapToFile(scaledCropBitmap, cropTempFile); +// // 回收临时Bitmap,避免内存泄漏 +// if (scaledCropBitmap != cropBitmap && !scaledCropBitmap.isRecycled()) { +// scaledCropBitmap.recycle(); +// } +// if (!cropBitmap.isRecycled()) { +// cropBitmap.recycle(); +// } +// } +// } +// +// // 延迟加载裁剪图(仅通知加载,不传递比例,BackgroundView自主读取文件计算比例) +// new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { +// @Override +// public void run() { +// if (mBackgroundView != null && !isFinishing()) { +// mBackgroundView.loadBackgroundBean(previewBean); // 仅传Bean(含文件路径),无比例传递 +// LogUtils.d(TAG, "裁剪后图片加载完成(延迟50ms,BackgroundView自主解析比例)"); +// } +// } +// }, 50); // 延迟50ms,确保系统裁剪文件写入完成 +// +// } else { +// // 其他失败场景 +// handleOperationCancelOrFail(); +// } +// } + + /** + * 核心新增方法:从系统裁剪文件中获取真实宽高比(独立计算,完全由系统裁剪应用决定) + * 不依赖任何外部类/参数,仅读取系统裁剪输出的文件尺寸 + * @param systemCropFile 系统裁剪应用输出的文件 + * @return 系统裁剪给出的宽高比(宽/高,失败返回-1) + */ +// private float getRatioFromSystemCropFile(File systemCropFile) { +// LogUtils.d(TAG, "【系统比例获取】从系统裁剪文件获取真实比例,文件路径:" + systemCropFile.getAbsolutePath()); +// if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) { +// LogUtils.e(TAG, "【系统比例获取】系统裁剪文件无效,无法获取比例"); +// return -1; +// } +// +// // Java7 语法:仅获取文件尺寸(不加载Bitmap,高效计算比例) +// BitmapFactory.Options options = new BitmapFactory.Options(); +// options.inJustDecodeBounds = true; // 仅读取尺寸,不占用内存 +// BitmapFactory.decodeFile(systemCropFile.getAbsolutePath(), options); +// +// int cropWidth = options.outWidth; +// int cropHeight = options.outHeight; +// if (cropWidth <= 0 || cropHeight <= 0) { +// LogUtils.e(TAG, "【系统比例获取】系统裁剪文件尺寸无效(宽:" + cropWidth + ",高:" + cropHeight + ")"); +// return -1; +// } +// +// // 计算系统裁剪给出的真实宽高比(宽/高) +// float systemRatio = (float) cropWidth / cropHeight; +// LogUtils.d(TAG, "【系统比例获取】成功,系统裁剪比例:" + systemRatio + "(宽:" + cropWidth + ",高:" + cropHeight + ")"); +// return systemRatio; +// } + + /** + * 核心新增方法:按系统裁剪比例调整Bitmap(仅对齐比例,不改变系统裁剪的主体内容) + * 独立方法,不依赖BackgroundView,确保2个类完全解耦 + * @param originalBitmap 系统裁剪输出的原始Bitmap + * @param systemCropRatio 系统裁剪给出的宽高比(宽/高) + * @return 按系统比例调整后的Bitmap + */ + private Bitmap adjustBitmapToSystemCropRatio(Bitmap originalBitmap, float systemCropRatio) { + LogUtils.d(TAG, "【系统比例调整】按系统裁剪比例对齐,目标比例:" + systemCropRatio); + if (originalBitmap == null || originalBitmap.isRecycled() || systemCropRatio <= 0) { + LogUtils.e(TAG, "【系统比例调整】参数无效,直接返回原Bitmap"); + return originalBitmap; + } + + int originalWidth = originalBitmap.getWidth(); + int originalHeight = originalBitmap.getHeight(); + float originalRatio = (float) originalWidth / originalHeight; + + // 若原比例与系统裁剪比例一致,无需调整(避免重复操作) + if (Math.abs(originalRatio - systemCropRatio) < 0.01f) { + LogUtils.d(TAG, "【系统比例调整】与系统裁剪比例一致,无需调整"); + return originalBitmap; + } + + // 按系统比例计算目标尺寸(居中裁剪,确保系统裁剪的主体内容不丢失) + int targetWidth, targetHeight; + if (originalRatio > systemCropRatio) { + // 原图更宽 → 按系统比例缩小宽度,保留高度(切除左右多余部分) + targetHeight = originalHeight; + targetWidth = Math.round(targetHeight * systemCropRatio); + } else { + // 原图更高 → 按系统比例缩小高度,保留宽度(切除上下多余部分) + targetWidth = originalWidth; + targetHeight = Math.round(targetWidth / systemCropRatio); + } + + LogUtils.d(TAG, "【系统比例调整】系统裁剪后尺寸:" + originalWidth + "x" + originalHeight + ",对齐后尺寸:" + targetWidth + "x" + targetHeight); + + // 居中裁剪(仅切除多余像素,不拉伸,保留系统裁剪的主体) + Bitmap adjustedBitmap = Bitmap.createBitmap( + originalBitmap, + (originalWidth - targetWidth) / 2, // 水平居中 + (originalHeight - targetHeight) / 2, // 垂直居中 + targetWidth, + targetHeight + ); + + LogUtils.d(TAG, "【系统比例调整】完成,最终比例与系统裁剪一致:" + (float) targetWidth / targetHeight); + return adjustedBitmap; + } + + /** + * 核心新增方法:将调整后的Bitmap保存到文件(覆盖系统裁剪输出的文件) + * 独立保存逻辑,不依赖任何外部类 + * @param bitmap 按系统比例调整后的Bitmap + * @param targetFile 系统裁剪输出的目标文件(覆盖保存) + */ +// private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) { +// LogUtils.d(TAG, "【图片保存】按系统裁剪比例保存,目标路径:" + targetFile.getAbsolutePath()); +// if (bitmap == null || bitmap.isRecycled() || targetFile == null) { +// LogUtils.e(TAG, "【图片保存】参数无效,保存失败"); +// return; +// } +// +// // 删除原系统裁剪文件(避免覆盖失败) +// if (targetFile.exists()) { +// boolean deleteSuccess = targetFile.delete(); +// LogUtils.d(TAG, "【图片保存】删除原系统裁剪文件:" + (deleteSuccess ? "成功" : "失败")); +// } +// +// // Java7 语法:保存Bitmap(JPEG格式,质量80%,平衡清晰度与文件大小) +// OutputStream outputStream = null; +// try { +// outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)); +// bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); +// outputStream.flush(); +// LogUtils.d(TAG, "【图片保存】成功,文件大小:" + targetFile.length() + "bytes,比例与系统裁剪一致"); +// } catch (IOException e) { +// LogUtils.e(TAG, "【图片保存】异常:" + e.getMessage(), e); +// } finally { +// // 手动关闭流(Java7 兼容写法,不使用try-with-resources) +// if (outputStream != null) { +// try { +// outputStream.close(); +// } catch (IOException e) { +// LogUtils.e(TAG, "【图片保存】关闭流异常:" + e.getMessage(), e); +// } +// } +// } +// } + + /** + * 解析裁剪临时文件为Bitmap(独立方法,仅处理系统裁剪文件,不依赖BackgroundView) + * @param cropTempFile 系统裁剪应用输出的临时文件 + * @return 解析后的Bitmap(失败返回null) + */ +// private Bitmap parseCropTempFileToBitmap(File cropTempFile) { +// // 第一重校验:文件对象有效性(规避空指针/非文件) +// if (cropTempFile == null) { +// LogUtils.e(TAG, "【裁剪文件解析】裁剪临时文件对象为空,解析失败"); +// return null; +// } +// if (!cropTempFile.exists()) { +// LogUtils.e(TAG, "【裁剪文件解析】裁剪临时文件不存在,路径:" + cropTempFile.getAbsolutePath()); +// return null; +// } +// if (!cropTempFile.isFile()) { +// LogUtils.e(TAG, "【裁剪文件解析】路径不是合法文件,路径:" + cropTempFile.getAbsolutePath()); +// return null; +// } +// if (cropTempFile.length() <= 100) { // 过滤小于100字节的空文件/破损文件 +// LogUtils.e(TAG, "【裁剪文件解析】裁剪文件过小(" + cropTempFile.length() + "bytes),视为无效文件"); +// return null; +// } +// +// LogUtils.d(TAG, "【裁剪文件解析】开始解析系统裁剪文件:" + cropTempFile.getAbsolutePath() + ",大小:" + cropTempFile.length() + "bytes"); +// +// // Java7 语法:Bitmap采样率加载(避免OOM) +// BitmapFactory.Options options = new BitmapFactory.Options(); +// // 第一步:仅获取图片尺寸,不加载完整Bitmap到内存 +// options.inJustDecodeBounds = true; +// BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); +// +// // 第二步:计算采样率(最大尺寸计算采样率(最大尺寸限制为2048px,适配所有机型) +// int maxSize = 2048; +// int sampleSize = 1; +// while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) { +// sampleSize *= 2; +// } +// sampleSize = Math.min(sampleSize, 16); // 限制最大采样率,避免模糊 +// LogUtils.d(TAG, "【裁剪文件解析】系统裁剪文件原始尺寸:" + options.outWidth + "x" + options.outHeight + ",采样率:" + sampleSize); +// +// // 第三步:设置采样率,加载完整Bitmap +// options.inJustDecodeBounds = false; +// options.inSampleSize = sampleSize; +// options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存 +// options.inPurgeable = true; +// options.inInputShareable = true; +// +// // 第四步:解析文件为Bitmap(捕获异常) +// Bitmap cropBitmap = null; +// try { +// cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); +// if (cropBitmap == null || cropBitmap.isRecycled()) { +// LogUtils.e(TAG, "【裁剪文件解析】解析失败,文件可能破损"); +// return null; +// } +// LogUtils.d(TAG, "【裁剪文件解析】成功,Bitmap内存大小:" + cropBitmap.getByteCount() / 1024 + "KB"); +// } catch (OutOfMemoryError e) { +// LogUtils.d(TAG, "【裁剪文件解析】OOM异常:" + e.getMessage(), Thread.currentThread().getStackTrace()); +// ToastUtils.show("图片解析失败,可能因图片过大"); +// return null; +// } catch (Exception e) { +// LogUtils.e(TAG, "【裁剪文件解析】未知异常:" + e.getMessage(), e); +// return null; +// } +// +// return cropBitmap; +// } + + + /** + * 启动系统裁剪(Java7 语法,记录接口比例参数,用于后续校验) + * @param isCropFree 是否自由裁剪(true:无固定比例,false:按视图比例) + */ + public void startCropImageActivity(boolean isCropFree) { + LogUtils.d(TAG, "startCropImageActivity: 启动系统裁剪,自由裁剪:" + isCropFree); + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundScaledCompressFile(false); + mBgSourceUtils.saveSettings(); + doubleRefreshPreview(); + + // 校验裁剪原图 + File previewFile = new File(previewBean.getBackgroundFilePath()); + if (previewFile == null || !previewFile.exists() || previewFile.length() <= 100) { + Toast.makeText(this, "无有效图片可裁剪", Toast.LENGTH_SHORT).show(); + LogUtils.e(TAG, "startCropImageActivity: 裁剪原图无效"); + return; + } + + // 生成裁剪输入Uri(适配MIUI权限) + Uri inputUri = null; + try { + inputUri = mBgSourceUtils.getFileProviderUri(previewFile); + // 授予系统裁剪工具权限(Java7 手动授予) + grantUriPermission("com.miui.gallery", inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + LogUtils.d(TAG, "startCropImageActivity: 输入Uri:" + inputUri.toString() + ",已授予MIUI权限"); + } catch (Exception e) { + LogUtils.e(TAG, "startCropImageActivity: 生成输入Uri失败:" + e.getMessage()); + Toast.makeText(this, "裁剪失败:无法获取图片权限", Toast.LENGTH_SHORT).show(); + return; + } + + // 生成裁剪输出临时文件 + File cropResultTempFile = new File(previewBean.getBackgroundScaledCompressFilePath()); + if (cropResultTempFile == null) { + Toast.makeText(this, "裁剪路径创建失败", Toast.LENGTH_SHORT).show(); + return; + } + + // 构建裁剪意图(Java7 语法,不使用Lambda) + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(inputUri, "image/*"); + intent.putExtra("crop", "true"); + intent.putExtra("noFaceDetection", true); + + // 关键:设置裁剪比例并记录(仅本类使用,不跨类传递) + if (!isCropFree) { + // 按视图尺寸计算比例(Java7 手动获取尺寸) + int viewWidth = mBackgroundView.getWidth() > 0 ? mBackgroundView.getWidth() : getResources().getDisplayMetrics().widthPixels; + int viewHeight = mBackgroundView.getHeight() > 0 ? mBackgroundView.getHeight() : getResources().getDisplayMetrics().heightPixels; + int gcd = calculateGCD(viewWidth, viewHeight); + // 记录传入系统裁剪的比例参数(临时变量,仅内部使用) + mCropAspectX = viewWidth / gcd; + mCropAspectY = viewHeight / gcd; + intent.putExtra("aspectX", mCropAspectX); + intent.putExtra("aspectY", mCropAspectY); + LogUtils.d(TAG, "startCropImageActivity: 传入系统裁剪接口比例:" + mCropAspectX + ":" + mCropAspectY); + } else { + // 自由裁剪:重置比例参数 + mCropAspectX = 0; + mCropAspectY = 0; + LogUtils.d(TAG, "startCropImageActivity: 启用自由裁剪(无固定比例)"); + } + + // 输出尺寸设置(Java7 手动计算) + int maxOutputSize = 2048; + int outputX = Math.min(getResources().getDisplayMetrics().widthPixels, maxOutputSize); + int outputY = Math.min(getResources().getDisplayMetrics().heightPixels, maxOutputSize); + intent.putExtra("outputX", outputX); + intent.putExtra("outputY", outputY); + intent.putExtra("scale", true); + intent.putExtra("scaleUpIfNeeded", true); + + // 输出配置(Java7 枚举值手动设置) + intent.putExtra("outputFormat", CompressFormat.JPEG.toString()); + intent.putExtra("quality", 100); // 100%质量,避免压缩偏差 + intent.putExtra("return-data", false); // 禁用返回Bitmap,避免OOM + + // 权限设置(Java7 手动添加Flags) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + + // 适配系统裁剪工具(Java7 手动遍历解析) + try { + List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (!resolveInfos.isEmpty()) { + ResolveInfo resolveInfo = resolveInfos.get(0); + String cropPackageName = resolveInfo.activityInfo.packageName; + String cropActivityName = resolveInfo.activityInfo.name; + Log.d(TAG, "startCropImageActivity: 找到裁剪工具:" + cropPackageName + "/" + cropActivityName); + + // 生成输出Uri并授予权限 + Uri outputUri = mBgSourceUtils.getFileProviderUri(cropResultTempFile); + if (cropPackageName.equals("com.miui.gallery")) { + grantUriPermission(cropPackageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + Log.d(TAG, "startCropImageActivity: 授予MIUI裁剪工具写入权限"); + } + + // 显式启动裁剪Activity(Java7 手动创建Component) + Intent cropIntent = new Intent(intent); + cropIntent.setComponent(new ComponentName(cropPackageName, cropActivityName)); + cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + // 保存输出路径 + previewBean.setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); + startActivityForResult(cropIntent, REQUEST_CROP_IMAGE); + Log.d(TAG, "startCropImageActivity: 启动系统裁剪,输出路径:" + cropResultTempFile.getAbsolutePath()); + } else { + // 兜底:启动第三方裁剪工具 + Uri outputUri = mBgSourceUtils.getFileProviderUri(cropResultTempFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + Intent chooser = Intent.createChooser(intent, "选择裁剪工具"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + previewBean.setBackgroundScaledCompressFilePath(cropResultTempFile.getAbsolutePath()); + if (chooser.resolveActivity(getPackageManager()) != null) { + startActivityForResult(chooser, REQUEST_CROP_IMAGE); + } else { + Toast.makeText(this, "无可用裁剪工具", Toast.LENGTH_SHORT).show(); + } + } + } catch (Exception e) { + Log.e(TAG, "startCropImageActivity: 启动裁剪失败:" + e.getMessage()); + Toast.makeText(this, "无法启动裁剪工具", Toast.LENGTH_SHORT).show(); + } + } + + + /** + * 处理裁剪结果(核心:闭环校验比例,确保系统接口→系统输出→保存文件 比例一致) + */ + private void handleCropImageResult(int requestCode, int resultCode, Intent data) { + Log.d(TAG, "handleCropImageResult: 处理裁剪结果"); + // 获取系统裁剪输出文件(仅读取文件,不依赖外部比例) + File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath()); + boolean isFileExist = cropTempFile.exists(); + boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; + long fileSize = isFileExist ? cropTempFile.length() : 0; + boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; + + if (isCropSuccess) { + Log.d(TAG, "handleCropImageResult: 裁剪成功,开始比例校验与保存"); + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundFile(true); + previewBean.setIsUseBackgroundScaledCompressFile(true); + mBgSourceUtils.saveSettings(); + + // 核心步骤1:获取两个关键比例(接口参数比例 + 系统输出比例) + float finalCropRatio = 0f; + float systemFileRatio = getRatioFromSystemCropFile(cropTempFile); // 系统输出文件比例 + float inputInterfaceRatio = (mCropAspectX > 0 && mCropAspectY > 0) ? (float) mCropAspectX / mCropAspectY : systemFileRatio; // 接口参数比例 + + // 核心步骤2:比例校验与确认(Java7 手动判断,不使用Lambda) + if (systemFileRatio > 0) { + if (inputInterfaceRatio > 0 && Math.abs(inputInterfaceRatio - systemFileRatio) < 0.001f) { + finalCropRatio = inputInterfaceRatio; + Log.d(TAG, "handleCropImageResult: 比例校验通过,接口与系统输出一致:" + finalCropRatio); + } else if (inputInterfaceRatio > 0 && Math.abs(inputInterfaceRatio - systemFileRatio) >= 0.001f) { + finalCropRatio = systemFileRatio; + Log.w(TAG, "handleCropImageResult: 接口与系统输出偏差,以系统为准:" + finalCropRatio); + } else { + finalCropRatio = systemFileRatio; + Log.d(TAG, "handleCropImageResult: 自由裁剪,使用系统比例:" + finalCropRatio); + } + } + + // 核心步骤3:按最终比例调整并保存(确保比例一致) + if (finalCropRatio > 0) { + Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); + if (cropBitmap != null && !cropBitmap.isRecycled()) { + Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, finalCropRatio); + saveScaledBitmapToFile(scaledCropBitmap, cropTempFile); + + // 回收临时Bitmap(Java7 手动回收) + if (scaledCropBitmap != cropBitmap && !scaledCropBitmap.isRecycled()) { + scaledCropBitmap.recycle(); + } + if (!cropBitmap.isRecycled()) { + cropBitmap.recycle(); + } + } + } + + // 延迟加载图片(仅传文件路径,BackgroundView自主解析) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + if (mBackgroundView != null && !isFinishing()) { + mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); + Log.d(TAG, "handleCropImageResult: 裁剪图片加载完成,比例已与系统一致"); + } + } + }, 50); + } else { + handleOperationCancelOrFail(); + } + } + + /** + * 从系统裁剪文件获取比例(Java7 语法,完全独立,不依赖外部) + */ + private float getRatioFromSystemCropFile(File systemCropFile) { + Log.d(TAG, "getRatioFromSystemCropFile: 读取系统裁剪文件比例,路径:" + systemCropFile.getAbsolutePath()); + if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) { + Log.e(TAG, "getRatioFromSystemCropFile: 文件无效"); + return -1; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(systemCropFile.getAbsolutePath(), options); + + int cropWidth = options.outWidth; + int cropHeight = options.outHeight; + if (cropWidth <= 0 || cropHeight <= 0) { + Log.e(TAG, "getRatioFromSystemCropFile: 尺寸无效:" + cropWidth + "x" + cropHeight); + return -1; + } + + float systemRatio = (float) cropWidth / cropHeight; + Log.d(TAG, "getRatioFromSystemCropFile: 系统裁剪比例:" + systemRatio + "(" + cropWidth + "x" + cropHeight + ")"); + return systemRatio; + } + + /** + * 按最终比例调整Bitmap(Java7 语法,高精度对齐) + */ + private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) { + Log.d(TAG, "adjustBitmapToFinalRatio: 调整比例,目标:" + finalCropRatio); + if (originalBitmap == null || originalBitmap.isRecycled() || finalCropRatio <= 0) { + Log.e(TAG, "adjustBitmapToFinalRatio: 参数无效"); + return originalBitmap; + } + + int originalWidth = originalBitmap.getWidth(); + int originalHeight = originalBitmap.getHeight(); + float originalRatio = (float) originalWidth / originalHeight; + + // 偏差≤0.001,视为一致,无需调整 + if (Math.abs(originalRatio - finalCropRatio) < 0.001f) { + Log.d(TAG, "adjustBitmapToFinalRatio: 比例一致,无需调整"); + return originalBitmap; + } + + // 计算目标尺寸(Java7 手动计算,精确到1px) + int targetWidth, targetHeight; + targetHeight = originalHeight; + targetWidth = Math.round(targetHeight * finalCropRatio); + if (targetWidth > originalWidth) { + targetWidth = originalWidth; + targetHeight = Math.round(targetWidth / finalCropRatio); + } + + // 强制修正尺寸,确保比例完全一致 + targetWidth = Math.round(targetHeight * finalCropRatio); + Log.d(TAG, "adjustBitmapToFinalRatio: 调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight); + + // 居中裁剪(Java7 手动计算偏移量) + Bitmap adjustedBitmap = Bitmap.createBitmap( + originalBitmap, + (originalWidth - targetWidth) / 2, + (originalHeight - targetHeight) / 2, + targetWidth, + targetHeight + ); + + // 最终校验 + float adjustedRatio = (float) adjustedBitmap.getWidth() / adjustedBitmap.getHeight(); + if (Math.abs(adjustedRatio - finalCropRatio) < 0.001f) { + Log.d(TAG, "adjustBitmapToFinalRatio: 调整完成,比例一致"); + } else { + Log.w(TAG, "adjustBitmapToFinalRatio: 存在微小偏差,已强制修正"); + } + return adjustedBitmap; + } + + /** + * 保存调整后的Bitmap(Java7 语法,100%质量,避免压缩偏差) + */ + private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) { + Log.d(TAG, "saveScaledBitmapToFile: 保存图片,路径:" + targetFile.getAbsolutePath()); + if (bitmap == null || bitmap.isRecycled() || targetFile == null) { + Log.e(TAG, "saveScaledBitmapToFile: 参数无效"); + return; + } + + // 删除原文件(Java7 手动删除) + if (targetFile.exists()) { + boolean deleteSuccess = targetFile.delete(); + Log.d(TAG, "saveScaledBitmapToFile: 删除原文件:" + (deleteSuccess ? "成功" : "失败")); + } + + OutputStream outputStream = null; + try { + // Java7 手动创建流,不使用try-with-resources + outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)); + bitmap.compress(CompressFormat.JPEG, 100, outputStream); // 100%质量 + outputStream.flush(); + Log.d(TAG, "saveScaledBitmapToFile: 保存成功,文件大小:" + targetFile.length() + "bytes"); + + // 保存后校验比例 + float savedRatio = getRatioFromSystemCropFile(targetFile); + if (savedRatio > 0 && Math.abs(savedRatio - (float) bitmap.getWidth()/bitmap.getHeight()) < 0.001f) { + Log.d(TAG, "saveScaledBitmapToFile: 保存后比例校验通过"); + } else { + Log.w(TAG, "saveScaledBitmapToFile: 保存后比例存在偏差"); + } + } catch (IOException e) { + Log.e(TAG, "saveScaledBitmapToFile: 保存异常:" + e.getMessage()); + } finally { + // Java7 手动关闭流,避免资源泄漏 + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + Log.e(TAG, "saveScaledBitmapToFile: 关闭流异常:" + e.getMessage()); + } + } + } + } + + /** + * 解析裁剪文件为Bitmap(Java7 语法,采样率加载) + */ + private Bitmap parseCropTempFileToBitmap(File cropTempFile) { + Log.d(TAG, "parseCropTempFileToBitmap: 解析裁剪文件:" + cropTempFile.getAbsolutePath()); + if (cropTempFile == null || !cropTempFile.exists() || !cropTempFile.isFile() || cropTempFile.length() <= 100) { + Log.e(TAG, "parseCropTempFileToBitmap: 文件无效"); + return null; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + + // 计算采样率(Java7 普通while循环) + int maxSize = 2048; + int sampleSize = 1; + while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) { + sampleSize *= 2; + } + sampleSize = Math.min(sampleSize, 16); + Log.d(TAG, "parseCropTempFileToBitmap: 采样率:" + sampleSize + ",原始尺寸:" + options.outWidth + "x" + options.outHeight); + + options.inJustDecodeBounds = false; + options.inSampleSize = sampleSize; + options.inPreferredConfig = Bitmap.Config.RGB_565; + options.inPurgeable = true; + options.inInputShareable = true; + + Bitmap cropBitmap = null; + try { + cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + if (cropBitmap == null || cropBitmap.isRecycled()) { + Log.e(TAG, "parseCropTempFileToBitmap: 解析失败"); + return null; + } + Log.d(TAG, "parseCropTempFileToBitmap: 解析成功,尺寸:" + cropBitmap.getWidth() + "x" + cropBitmap.getHeight()); + } catch (OutOfMemoryError e) { + Log.e(TAG, "parseCropTempFileToBitmap: OOM异常:" + e.getMessage()); + Toast.makeText(this, "图片解析失败,可能过大", Toast.LENGTH_SHORT).show(); + return null; + } catch (Exception e) { + Log.e(TAG, "parseCropTempFileToBitmap: 解析异常:" + e.getMessage()); + return null; + } + + return cropBitmap; + } + } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java index 7504073b..81459531 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java @@ -60,6 +60,6 @@ public class BackgroundViewTestFragment extends Fragment { void loadBackground() { - mBackgroundView.loadImage("/storage/emulated/0/Pictures/Gallery/owner/素材/1626915857361.png"); + //mBackgroundView.loadImage("/storage/emulated/0/Pictures/Gallery/owner/素材/1626915857361.png"); } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index 0fc36547..aa1b0868 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; +import android.os.Build; +import androidx.core.content.FileProvider; /** * @Author ZhanGSKen @@ -216,7 +218,32 @@ public class BackgroundSourceUtils { clearCropTempFiles(); LogUtils.d(TAG, "【文件初始化】完成。"); } - + + // 【核心实现】定义 getFileProviderUri 方法:将 File 转为 ContentUri(适配 FileProvider) + public Uri getFileProviderUri(File file) { + Log.d("BackgroundSourceUtils", "getFileProviderUri: 生成FileProvider Uri,文件路径:" + file.getAbsolutePath()); + Uri contentUri = null; + try { + // 适配 Android 7.0+:使用 FileProvider 生成 ContentUri(避免 FileUriExposedException) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + contentUri = FileProvider.getUriForFile( + mContext, + FILE_PROVIDER_AUTHORITY, // 与清单文件中一致 + file + ); + Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0+ 生成ContentUri:" + contentUri.toString()); + } else { + // 适配 Android 7.0 以下:直接使用 File.toURI()(兼容旧版本) + contentUri = Uri.fromFile(file); + Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0以下 生成FileUri:" + contentUri.toString()); + } + } catch (IllegalArgumentException e) { + // 捕获异常(如文件路径无效、授权不匹配等) + Log.e("BackgroundSourceUtils", "getFileProviderUri: 生成Uri失败,异常:" + e.getMessage(), e); + contentUri = null; + } + return contentUri; + } public boolean createCropFileProviderPath(Uri uri) { InputStream is = null; diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java index 214b5e66..07fc3ff0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -3,252 +3,195 @@ package cc.winboll.studio.powerbell.views; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.text.TextUtils; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; import android.util.AttributeSet; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.powerbell.model.BackgroundBean; +import android.view.View; +import android.util.Log; + import java.io.File; +import cc.winboll.studio.powerbell.model.BackgroundBean; /** - * 基于Java7的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺) - * 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪 + * 图片背景设置视图(完全独立类,不依赖BackgroundSettingsActivity) + * 仅通过文件路径自主解析比例,不接收任何外部比例参数 */ -public class BackgroundView extends RelativeLayout { +public class BackgroundView extends View { + private static final String TAG = "BackgroundView"; + private Bitmap mBackgroundBitmap; // 背景图片Bitmap + private Paint mPaint; // 绘制画笔 + private RectF mDrawRect; // 绘制区域 + private Matrix mMatrix; // 缩放矩阵 - public static final String TAG = "BackgroundView"; - - 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 启动 ==="); - this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs) { super(context, attrs); - LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ==="); - this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ==="); - this.mContext = context; initView(); } - // ====================================== 初始化 ====================================== - private void initView() { - LogUtils.d(TAG, "=== initView 启动 ==="); - // 1. 配置当前控件:全屏+透明 - setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - setBackgroundColor(0x00000000); - setBackground(new ColorDrawable(0x00000000)); - - // 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 - ); - mLlContainer.setLayoutParams(llParams); - mLlContainer.setOrientation(LinearLayout.VERTICAL); - mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中 - mLlContainer.setBackgroundColor(0x00000000); - this.addView(mLlContainer); - LogUtils.d(TAG, "=== initLinearLayout 完成 ==="); - } - - private void initImageView() { - LogUtils.d(TAG, "=== initImageView 启动 ==="); - mIvBackground = new ImageView(mContext); - // 配置ImageView:wrap_content+居中+透明背景 - LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ); - mIvBackground.setLayoutParams(ivParams); - mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺 - mIvBackground.setBackgroundColor(0x00000000); - mLlContainer.addView(mIvBackground); - LogUtils.d(TAG, "=== initImageView 完成 ==="); - } - - public void loadBackgroundBean(BackgroundBean bean) { - if(!bean.isUseBackgroundFile()) { - setDefaultTransparentBackground(); - return; - } - if(bean.isUseBackgroundScaledCompressFile()) { - loadImage(bean.getBackgroundScaledCompressFilePath()); - } else { - - loadImage(bean.getBackgroundFilePath()); - } - } - - // ====================================== 对外方法 ====================================== /** - * 加载图片(保持原图比例,在LinearLayout中居中平铺) - * @param imagePath 图片绝对路径 + * 初始化视图(Java7语法,无Lambda) */ - public void loadImage(String imagePath) { - LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ==="); - if (TextUtils.isEmpty(imagePath)) { - setDefaultTransparentBackground(); - return; - } - - File imageFile = new File(imagePath); - if (!imageFile.exists() || !imageFile.isFile()) { - LogUtils.e(TAG, "图片文件无效"); - setDefaultTransparentBackground(); - return; - } - - // 计算原图比例 - if (!calculateImageAspectRatio(imageFile)) { - setDefaultTransparentBackground(); - return; - } - - // 压缩加载Bitmap - Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920); - if (bitmap == null) { - setDefaultTransparentBackground(); - return; - } - - // 设置图片 - mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap)); - adjustImageViewSize(); // 调整尺寸 - LogUtils.d(TAG, "=== loadImage 完成 ==="); + private void initView() { + Log.d(TAG, "initView: 初始化BackgroundView"); + mPaint = new Paint(); + mPaint.setAntiAlias(true); // 抗锯齿 + mPaint.setFilterBitmap(true); // 过滤 bitmap,使绘制更清晰 + mDrawRect = new RectF(); + mMatrix = new Matrix(); } - // ====================================== 内部工具方法 ====================================== - private boolean calculateImageAspectRatio(File file) { - 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, "图片尺寸无效"); - return false; - } - - mImageAspectRatio = (float) width / height; - LogUtils.d(TAG, "原图比例:" + mImageAspectRatio); - return true; - } catch (Exception e) { - LogUtils.e(TAG, "计算比例失败:" + e.getMessage()); - return false; + /** + * 加载背景图片(仅接收BackgroundBean,仅读取文件路径,自主解析比例) + * @param bean 背景图片信息(含文件路径,无比例信息) + */ + public void loadBackgroundBean(BackgroundBean bean) { + Log.d(TAG, "loadBackgroundBean: 开始加载背景图片,文件路径:" + bean.getBackgroundScaledCompressFilePath()); + // 回收旧Bitmap,避免内存泄漏(Java7 手动回收) + if (mBackgroundBitmap != null && !mBackgroundBitmap.isRecycled()) { + mBackgroundBitmap.recycle(); + mBackgroundBitmap = null; + Log.d(TAG, "loadBackgroundBean: 回收旧背景Bitmap"); } + + // 读取文件路径,自主解析图片(完全独立,不依赖外部比例) + String imagePath = bean.getBackgroundScaledCompressFilePath(); + if (imagePath != null && !imagePath.isEmpty()) { + File imageFile = new File(imagePath); + mBackgroundBitmap = decodeBitmapFromFile(imageFile); + Log.d(TAG, "loadBackgroundBean: 背景图片加载完成,是否成功:" + (mBackgroundBitmap != null)); + } else { + Log.e(TAG, "loadBackgroundBean: 图片路径为空,加载失败"); + } + + // 刷新视图 + invalidate(); } - private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) { - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - - int scaleX = options.outWidth / maxWidth; - int scaleY = options.outHeight / maxHeight; - int inSampleSize = Math.max(scaleX, scaleY); - if (inSampleSize <= 0) inSampleSize = 1; - - options.inJustDecodeBounds = false; - options.inSampleSize = inSampleSize; - options.inPreferredConfig = Bitmap.Config.RGB_565; - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } catch (Exception e) { - LogUtils.e(TAG, "压缩解码失败:" + e.getMessage()); + /** + * 解析图片文件(Java7语法,采样率加载,自主计算比例) + * @param imageFile 图片文件(来自BackgroundBean的路径) + * @return 解析后的Bitmap(失败返回null) + */ + private Bitmap decodeBitmapFromFile(File imageFile) { + Log.d(TAG, "decodeBitmapFromFile: 解析图片文件,路径:" + imageFile.getAbsolutePath()); + // 校验文件有效性 + if (imageFile == null || !imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 100) { + Log.e(TAG, "decodeBitmapFromFile: 图片文件无效(不存在/非文件/过小)"); return null; } + + // Java7 语法:Bitmap采样率加载(避免OOM) + BitmapFactory.Options options = new BitmapFactory.Options(); + // 第一步:仅获取图片尺寸,不加载完整Bitmap + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); + + // 第二步:计算采样率(最大尺寸限制为2048px,适配所有机型) + int maxSize = 2048; + int sampleSize = 1; + // Java7 普通while循环,不使用增强for/ Lambda + while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) { + sampleSize *= 2; + } + sampleSize = Math.min(sampleSize, 16); // 限制最大采样率,避免模糊 + Log.d(TAG, "decodeBitmapFromFile: 图片原始尺寸:" + options.outWidth + "x" + options.outHeight + ",采样率:" + sampleSize); + + // 第三步:设置采样率,加载完整Bitmap + options.inJustDecodeBounds = false; + options.inSampleSize = sampleSize; + options.inPreferredConfig = Bitmap.Config.RGB_565; // 节省内存(Java7 兼容配置) + options.inPurgeable = true; // 允许内存不足时回收 + options.inInputShareable = true; // 配合inPurgeable使用 + + // 第四步:解析文件(捕获异常,Java7 手动捕获) + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); + if (bitmap == null || bitmap.isRecycled()) { + Log.e(TAG, "decodeBitmapFromFile: 解析失败,Bitmap为空或已回收"); + return null; + } + Log.d(TAG, "decodeBitmapFromFile: 解析成功,Bitmap尺寸:" + bitmap.getWidth() + "x" + bitmap.getHeight() + ",内存大小:" + bitmap.getByteCount() / 1024 + "KB"); + } catch (OutOfMemoryError e) { + Log.e(TAG, "decodeBitmapFromFile: OOM异常,图片过大:" + e.getMessage()); + return null; + } catch (Exception e) { + Log.e(TAG, "decodeBitmapFromFile: 解析异常:" + e.getMessage()); + return null; + } + + return bitmap; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + // 获取视图实际尺寸 + int viewWidth = MeasureSpec.getSize(widthMeasureSpec); + int viewHeight = MeasureSpec.getSize(heightMeasureSpec); + Log.d(TAG, "onMeasure: 视图尺寸:" + viewWidth + "x" + viewHeight); + mDrawRect.set(0, 0, viewWidth, viewHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + Log.d(TAG, "onDraw: 绘制背景图片"); + // 若Bitmap为空,不绘制 + if (mBackgroundBitmap == null || mBackgroundBitmap.isRecycled()) { + Log.w(TAG, "onDraw: 背景Bitmap为空,跳过绘制"); + return; + } + + // 计算缩放比例(自主计算,基于视图尺寸和图片尺寸,无外部依赖) + float viewWidth = mDrawRect.width(); + float viewHeight = mDrawRect.height(); + float bitmapWidth = mBackgroundBitmap.getWidth(); + float bitmapHeight = mBackgroundBitmap.getHeight(); + + // 计算缩放比例(保持比例,填满视图,裁剪多余部分) + float scaleX = viewWidth / bitmapWidth; + float scaleY = viewHeight / bitmapHeight; + float scale = Math.max(scaleX, scaleY); // 取较大缩放比,确保填满视图 + + // 计算偏移量(水平/垂直居中) + float translateX = (viewWidth - bitmapWidth * scale) / 2; + float translateY = (viewHeight - bitmapHeight * scale) / 2; + + // 设置矩阵(Java7 手动设置,不使用Lambda) + mMatrix.reset(); + mMatrix.postScale(scale, scale); + mMatrix.postTranslate(translateX, translateY); + + // 绘制Bitmap + canvas.drawBitmap(mBackgroundBitmap, mMatrix, mPaint); + Log.d(TAG, "onDraw: 背景图片绘制完成,缩放比:" + scale + ",偏移量:(" + translateX + "," + translateY + ")"); } /** - * 调整ImageView尺寸(保持原图比例,在LinearLayout中居中平铺) + * 回收资源(Java7 手动回收,避免内存泄漏) */ - private void adjustImageViewSize() { - LogUtils.d(TAG, "=== adjustImageViewSize 启动 ==="); - if (mLlContainer == null || mIvBackground == null) { - LogUtils.e(TAG, "控件为空"); - return; + public void releaseResource() { + Log.d(TAG, "releaseResource: 回收BackgroundView资源"); + if (mBackgroundBitmap != null && !mBackgroundBitmap.isRecycled()) { + mBackgroundBitmap.recycle(); + mBackgroundBitmap = null; } - - // 获取LinearLayout尺寸 - int llWidth = mLlContainer.getWidth(); - int llHeight = mLlContainer.getHeight(); - if (llWidth == 0 || llHeight == 0) { - postDelayed(new Runnable() { - @Override - public void run() { - adjustImageViewSize(); - } - }, 10); - return; - } - - // 计算ImageView尺寸(保持比例,不超出LinearLayout) - int ivWidth, ivHeight; - if (mImageAspectRatio >= 1.0f) { - ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth); - ivHeight = (int) (ivWidth / mImageAspectRatio); - } else { - 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); // 确保居中平铺 - - LogUtils.d(TAG, "ImageView尺寸:" + ivWidth + "x" + ivHeight); - LogUtils.d(TAG, "=== adjustImageViewSize 完成 ==="); - } - - private void setDefaultTransparentBackground() { - mIvBackground.setImageBitmap(null); - mIvBackground.setBackgroundColor(0x00000000); - mImageAspectRatio = 1.0f; - } - - // ====================================== 重写方法 ====================================== - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - adjustImageViewSize(); // 尺寸变化时重新调整 + mPaint = null; + mDrawRect = null; + mMatrix = null; } } + diff --git a/powerbell/src/main/res/layout/activity_background_settings.xml b/powerbell/src/main/res/layout/activity_background_settings.xml index 989e7ee0..c97f8e3e 100644 --- a/powerbell/src/main/res/layout/activity_background_settings.xml +++ b/powerbell/src/main/res/layout/activity_background_settings.xml @@ -32,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FF3243E2" - android:id="@+id/activitybackgroundpictureBackgroundView1"> + android:id="@+id/background_view">