From a997fb01c8d876a5c8236e92d51e29a9f24e28a0 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Tue, 2 Dec 2025 13:09:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../powerbell/views/BackgroundView.java | 566 +++++++++++------- 1 file changed, 339 insertions(+), 227 deletions(-) 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 745a5d61..f1102a30 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 @@ -21,427 +21,539 @@ import androidx.annotation.Nullable; * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/19 18:01 * @Describe 背景图片视图控件(全透明背景 + 保持比例扩展 + 完全填充父视图 + 预览/正式模式切换) + * 核心功能:统一处理背景图片的加载、比例适配、模式切换,适配多布局场景和低版本Android系统 */ public class BackgroundView extends RelativeLayout { - public static final String TAG = "BackgroundView"; + public static final String TAG = "BackgroundView"; // 日志标记 + // 上下文对象(全局复用) private Context mContext; + // 背景图片显示控件(核心子View) private ImageView ivBackground; - private BackgroundSourceUtils backgroundSourceUtils; // 工具类实例(避免重复创建) + // 背景资源工具类(单例实例,避免重复创建) + private BackgroundSourceUtils backgroundSourceUtils; - private float imageAspectRatio = 1.0f; // 图片原始宽高比(控制不拉伸的核心) - // 标记当前是否处于预览模式(用于区分加载预览/正式背景) + // 图片原始宽高比(控制图片不拉伸的核心参数,宽/高) + private float imageAspectRatio = 1.0f; + // 预览模式标记(true:预览模式,false:正式模式,区分加载不同背景资源) private boolean isPreviewMode = false; - // 构造器(兼容所有布局场景) + // ====================================== 构造器(兼容所有布局场景)====================================== + /** + * 构造器1:代码创建控件时调用 + * @param context 上下文 + */ public BackgroundView(Context context) { super(context); + LogUtils.d(TAG, "=== BackgroundView 构造器1(代码创建)启动 ==="); this.mContext = context; initView(); } + /** + * 构造器2:XML布局中使用时调用(无自定义属性) + * @param context 上下文 + * @param attrs 属性集 + */ public BackgroundView(Context context, AttributeSet attrs) { super(context, attrs); + LogUtils.d(TAG, "=== BackgroundView 构造器2(XML无属性)启动 ==="); this.mContext = context; initView(); } + /** + * 构造器3:XML布局中使用时调用(带自定义属性+默认样式) + * @param context 上下文 + * @param attrs 属性集 + * @param defStyleAttr 默认样式属性 + */ public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + LogUtils.d(TAG, "=== BackgroundView 构造器3(XML带属性+默认样式)启动 ==="); this.mContext = context; initView(); } + /** + * 构造器4:XML布局中使用时调用(带自定义属性+默认样式+自定义样式资源) + * @param context 上下文 + * @param attrs 属性集 + * @param defStyleAttr 默认样式属性 + * @param defStyleRes 自定义样式资源 + */ public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + LogUtils.d(TAG, "=== BackgroundView 构造器4(XML全参数)启动 ==="); this.mContext = context; initView(); } + // ====================================== 初始化相关方法 ====================================== /** - * 初始化视图(控件本身+内部ImageView) + * 初始化视图(控件本身配置 + 子View初始化 + 初始背景加载) + * 所有构造器统一调用此方法,避免重复代码 */ private void initView() { - // 1. 控件本身配置:完全填充父视图 + 全透明背景 + 无内边距 + LogUtils.d(TAG, "=== initView(视图初始化)启动 ==="); + // 1. 配置当前控件:完全填充父视图 + 全透明背景 + 无内边距 setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - setPadding(0, 0, 0, 0); // 取消自身内边距(避免父容器与控件间缝隙) - setBackgroundColor(0x00000000); // 全透明背景(ARGB:透明+黑色,无视觉影响) + setPadding(0, 0, 0, 0); // 取消自身内边距,避免父容器与控件间出现缝隙 + setBackgroundColor(0x00000000); // 全透明背景(ARGB:透明通道+黑色,无视觉影响) setBackground(new ColorDrawable(0x00000000)); // 双重保障:兼容Android低版本,确保背景透明 - // 初始化工具类(单例,全局唯一) + // 2. 初始化背景资源工具类(单例模式,全局唯一实例) backgroundSourceUtils = BackgroundSourceUtils.getInstance(mContext); - // 2. 初始化内部ImageView(核心:保持比例+居中+全透明) + // 3. 初始化内部ImageView(背景图片显示核心控件) initBackgroundImageView(); - // 3. 初始加载:默认加载正式背景 + // 4. 初始加载:默认加载正式模式背景 reloadCurrentBackground(); + LogUtils.d(TAG, "=== initView(视图初始化)完成 ==="); } /** - * 初始化内部ImageView(配置尺寸、缩放模式、背景等) + * 初始化内部ImageView(配置尺寸、缩放模式、背景等基础属性) + * 单独抽离方法,提高代码可读性 */ private void initBackgroundImageView() { + LogUtils.d(TAG, "=== initBackgroundImageView(内部ImageView初始化)启动 ==="); ivBackground = new ImageView(mContext); - // 基础布局:宽高WRAP_CONTENT(跟随图片比例)+ 居中显示 + 无内边距/边距 + // 配置ImageView布局参数:宽高自适应 + 居中显示 + 无内边距/外边距 RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); // 核心:ImageView在控件中居中 - layoutParams.setMargins(0, 0, 0, 0); // 取消边距(避免ImageView与控件间缝隙) + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); // 核心:让ImageView在当前控件中居中 + layoutParams.setMargins(0, 0, 0, 0); // 取消外边距,避免ImageView与控件间出现缝隙 ivBackground.setLayoutParams(layoutParams); - // 关键配置:缩放模式FIT_CENTER(保持比例,完整显示图片),与正式模式统一 - ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER); - ivBackground.setPadding(0, 0, 0, 0); // 取消内部padding(避免图片与ImageView间缝隙) - ivBackground.setBackgroundColor(0x00000000); // ImageView背景全透明 - ivBackground.setBackground(new ColorDrawable(0x00000000)); // 低版本兼容 + // 配置ImageView显示属性:保持比例 + 全透明背景 + ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER); // 缩放模式:保持比例,完整显示图片 + ivBackground.setPadding(0, 0, 0, 0); // 取消内边距,避免图片与ImageView间出现缝隙 + ivBackground.setBackgroundColor(0x00000000); // ImageView自身背景全透明 + ivBackground.setBackground(new ColorDrawable(0x00000000)); // 低版本兼容,确保透明 - this.addView(ivBackground); // 添加到父容器(控件本身) + // 将ImageView添加到当前控件(父容器) + this.addView(ivBackground); + LogUtils.d(TAG, "=== initBackgroundImageView(内部ImageView初始化)完成 ==="); } + // ====================================== 对外提供的公共方法 ====================================== /** - * 【对外提供】重新加载正式背景图片(从正式Bean获取路径) - * 用于:退出预览模式、恢复默认背景、正式背景更新后刷新 + * 【对外提供】重新加载正式模式背景图片 + * 适用场景:退出预览模式、恢复默认背景、正式背景资源更新后刷新显示 */ public void reloadCurrentBackground() { - LogUtils.d(TAG, "=== 开始重新加载正式背景 ==="); + LogUtils.d(TAG, "=== reloadCurrentBackground(重新加载正式背景)启动 ==="); isPreviewMode = false; // 标记为正式模式 - // 从工具类获取最新正式背景路径(确保路径同步) + // 从工具类获取最新的正式背景图片路径(确保路径与全局同步) String backgroundPath = backgroundSourceUtils.getCurrentBackgroundFilePath(); - // 加载并显示图片(复用正式模式的比例计算逻辑) + // 加载并显示图片(复用统一的加载逻辑) loadAndSetImageViewBackground(backgroundPath); + LogUtils.d(TAG, "=== reloadCurrentBackground(重新加载正式背景)完成 ==="); } - /** - * 重新加载预览背景(修复:复用比例计算逻辑+保持图片原始比例) - * 作用:控件加载图片时,优先用压缩图路径,失败则用原图路径,均失败则显示默认图 - */ - public void reloadPreviewBackground() { - LogUtils.d(TAG, "=== 开始加载预览背景(比例适配版)==="); - // 关键:直接从工具类获取预览Bean最新路径(避免使用缓存路径) - BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(getContext()); - BackgroundBean previewBean = bgSourceUtils.getPreviewBackgroundBean(); - if (previewBean == null) { - LogUtils.e(TAG, "【加载失败】预览Bean为空,显示默认图"); - setDefaultBackground(); - return; - } + /** + * 【对外提供】重新加载预览模式背景图片 + * 逻辑:优先加载压缩图(省内存)→ 压缩图无效则加载原图 → 均无效则显示默认透明背景 + */ + public void reloadPreviewBackground() { + LogUtils.d(TAG, "=== reloadPreviewBackground(重新加载预览背景)启动 ==="); + // 获取背景资源工具类实例(单例) + BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(getContext()); + // 获取预览模式对应的背景Bean(存储预览图路径等信息) + BackgroundBean previewBean = bgSourceUtils.getPreviewBackgroundBean(); - // 1. 优先加载压缩图路径(预览首选,节省内存) - String compressPath = previewBean.getBackgroundScaledCompressFilePath(); - File compressFile = checkFileValidity(compressPath); // 校验文件有效性 - if (compressFile != null) { - // 修复:调用带文件的加载方法(复用比例计算逻辑) - loadPreviewImageByFile(compressFile, "压缩图"); - return; - } + // 校验预览Bean是否为空,为空则直接显示默认背景 + if (previewBean == null) { + LogUtils.e(TAG, "【reloadPreviewBackground】预览Bean为空,无法加载预览图"); + setDefaultBackground(); + LogUtils.d(TAG, "=== reloadPreviewBackground(重新加载预览背景)完成(预览Bean为空)==="); + return; + } - // 2. 兜底加载原图路径(压缩图失败时) - LogUtils.w(TAG, "【压缩图无效】尝试加载原图路径"); - String originalPath = previewBean.getBackgroundFilePath(); - File originalFile = checkFileValidity(originalPath); - if (originalFile != null) { - loadPreviewImageByFile(originalFile, "原图"); - return; - } + // 1. 优先加载压缩图路径(预览模式首选,节省内存) + String compressPath = previewBean.getBackgroundScaledCompressFilePath(); + File compressFile = checkFileValidity(compressPath); // 校验压缩图文件有效性 + if (compressFile != null) { + loadPreviewImageByFile(compressFile, "压缩图"); // 加载压缩图 + LogUtils.d(TAG, "=== reloadPreviewBackground(重新加载预览背景)完成(加载压缩图成功)==="); + return; + } - // 3. 终极兜底:显示默认图(避免白色) - LogUtils.e(TAG, "【加载失败】压缩图和原图均无效,显示默认图"); - setDefaultBackground(); - } + // 2. 压缩图无效时,兜底加载原图路径 + LogUtils.w(TAG, "【reloadPreviewBackground】压缩图无效,尝试加载原图"); + String originalPath = previewBean.getBackgroundFilePath(); + File originalFile = checkFileValidity(originalPath); // 校验原图文件有效性 + if (originalFile != null) { + loadPreviewImageByFile(originalFile, "原图"); // 加载原图 + LogUtils.d(TAG, "=== reloadPreviewBackground(重新加载预览背景)完成(加载原图成功)==="); + return; + } - /** - * 新增:预览模式加载图片(按文件加载,复用比例计算逻辑) - * 核心:计算图片原始宽高比,确保ImageView按比例扩展到父控件 - */ - private void loadPreviewImageByFile(File imageFile, String imageType) { - LogUtils.d(TAG, "【" + imageType + "加载】开始加载:" + imageFile.getAbsolutePath()); - isPreviewMode = true; // 标记为预览模式 - - // 1. 计算图片原始宽高比(核心:保持比例的关键) - if (!calculateImageAspectRatio(imageFile)) { - LogUtils.e(TAG, "【" + imageType + "加载失败】图片尺寸无效"); - setDefaultBackground(); - return; - } - - // 2. 压缩加载Bitmap(避免OOM,保持原始比例) - Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920); - if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { - LogUtils.e(TAG, "【" + imageType + "加载失败】Bitmap无效(空/宽高为0)"); - setDefaultBackground(); - return; - } - - // 2. 配置ImageView(保持比例,与正式模式一致) - ivBackground.setScaleType(ScaleType.FIT_CENTER); // 修复:取消CENTER_CROP,用FIT_CENTER保持比例 - ivBackground.setBackgroundColor(0x00000000); // 确保背景透明,不覆盖图片 - ivBackground.setImageBitmap(bitmap); // 设置图片 - - // 3. 调整ImageView尺寸(按原始比例扩展到父控件) - adjustImageViewSize(); - - LogUtils.d(TAG, "【" + imageType + "加载成功】宽高:" + bitmap.getWidth() + "x" + bitmap.getHeight() + ",宽高比:" + imageAspectRatio); - } - - /** - * 新增:校验文件有效性(路径非空+文件存在+是文件+大小>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; - } - - /** - * 兜底:无默认图片时,用代码生成透明背景(避免白色/拉伸) - */ - private void setDefaultBackground() { - isPreviewMode = true; - setBackgroundColor(0x00000000); // 全透明背景 - adjustImageViewSize(); // 调整尺寸,确保居中 - LogUtils.d(TAG, "【默认背景】使用透明背景(无图片资源)"); - } + // 3. 压缩图和原图均无效时,显示默认透明背景 + LogUtils.e(TAG, "【reloadPreviewBackground】压缩图和原图均无效,显示默认背景"); + setDefaultBackground(); + LogUtils.d(TAG, "=== reloadPreviewBackground(重新加载预览背景)完成(显示默认背景)==="); + } /** - * 【对外提供】预览指定路径的临时图片(直接传入路径,不依赖Bean) - * 用于:临时预览本地图片、测试图片等场景 - * @param previewImagePath 临时图片绝对路径(空则显示透明) + * 【对外提供】预览指定路径的临时图片(直接传入路径,不依赖BackgroundBean) + * 适用场景:临时预览本地图片、测试图片等无需存入Bean的场景 + * @param previewImagePath 临时图片的绝对路径(为空则显示透明背景) */ public void previewBackgroundImage(String previewImagePath) { - LogUtils.d(TAG, "=== 开始预览指定路径图片 ==="); + LogUtils.d(TAG, "=== previewBackgroundImage(预览指定路径图片)启动 ==="); isPreviewMode = true; // 标记为预览模式 - // 加载并显示指定路径图片(复用正式模式的比例计算逻辑) + // 加载并显示指定路径的图片(复用统一的加载逻辑) loadAndSetImageViewBackground(previewImagePath); + LogUtils.d(TAG, "=== previewBackgroundImage(预览指定路径图片)完成 ==="); } /** - * 【核心逻辑】加载图片并设置到ImageView(统一处理,避免重复代码) + * 【对外提供】获取当前是否处于预览模式 + * 适用场景:Activity中判断当前背景模式,决定是否提交预览背景到正式模式 + * @return true:预览模式,false:正式模式 + */ + public boolean isPreviewMode() { + LogUtils.d(TAG, "=== isPreviewMode(获取当前模式)启动 ==="); + LogUtils.d(TAG, "【isPreviewMode】当前模式:" + (isPreviewMode ? "预览模式" : "正式模式")); + LogUtils.d(TAG, "=== isPreviewMode(获取当前模式)完成 ==="); + return isPreviewMode; + } + + // ====================================== 内部私有工具方法 ====================================== + /** + * 预览模式专用:按文件加载图片(复用比例计算逻辑,确保图片不拉伸) + * @param imageFile 待加载的图片文件 + * @param imageType 图片类型描述(如"压缩图"、"原图",用于日志区分) + */ + private void loadPreviewImageByFile(File imageFile, String imageType) { + LogUtils.d(TAG, "=== loadPreviewImageByFile(预览模式加载" + imageType + ")启动 ==="); + isPreviewMode = true; // 标记为预览模式(双重确认) + + // 1. 计算图片原始宽高比(核心:确保图片不拉伸的关键) + if (!calculateImageAspectRatio(imageFile)) { + LogUtils.e(TAG, "【loadPreviewImageByFile】" + imageType + "宽高比计算失败,图片尺寸无效"); + setDefaultBackground(); + LogUtils.d(TAG, "=== loadPreviewImageByFile(预览模式加载" + imageType + ")完成(尺寸无效)==="); + return; + } + + // 2. 压缩加载Bitmap(避免OOM,同时保持原始比例) + Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920); + // 校验Bitmap有效性(为空或宽高<=0均视为无效) + if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { + LogUtils.e(TAG, "【loadPreviewImageByFile】" + imageType + "加载失败,Bitmap无效"); + setDefaultBackground(); + LogUtils.d(TAG, "=== loadPreviewImageByFile(预览模式加载" + imageType + ")完成(Bitmap无效)==="); + return; + } + + // 3. 配置ImageView并设置图片(保持比例+全透明背景) + ivBackground.setScaleType(ScaleType.FIT_CENTER); // 缩放模式:保持比例,完整显示 + ivBackground.setBackgroundColor(0x00000000); // 确保ImageView背景透明,不覆盖图片 + ivBackground.setImageBitmap(bitmap); // 设置图片到ImageView + + // 4. 调整ImageView尺寸(按图片比例扩展到父控件,确保居中显示) + adjustImageViewSize(); + + LogUtils.d(TAG, "【loadPreviewImageByFile】" + imageType + "加载成功"); + LogUtils.d(TAG, "→ 图片尺寸:" + bitmap.getWidth() + "x" + bitmap.getHeight()); + LogUtils.d(TAG, "→ 图片宽高比:" + imageAspectRatio); + LogUtils.d(TAG, "=== loadPreviewImageByFile(预览模式加载" + imageType + ")完成(加载成功)==="); + } + + /** + * 校验图片文件有效性(路径非空+文件存在+是文件+大小>100bytes) + * 统一校验逻辑,避免重复代码 + * @param filePath 待校验的图片路径 + * @return 有效则返回File对象,无效则返回null + */ + @Nullable + private File checkFileValidity(String filePath) { + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)启动 ==="); + // 1. 校验路径是否为空 + if (TextUtils.isEmpty(filePath)) { + LogUtils.w(TAG, "【checkFileValidity】校验失败:图片路径为空"); + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)完成(路径为空)==="); + return null; + } + + File file = new File(filePath); + // 2. 校验文件是否存在 + if (!file.exists()) { + LogUtils.w(TAG, "【checkFileValidity】校验失败:文件不存在,路径:" + filePath); + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)完成(文件不存在)==="); + return null; + } + + // 3. 校验是否为文件(避免是文件夹) + if (!file.isFile()) { + LogUtils.w(TAG, "【checkFileValidity】校验失败:不是文件,路径:" + filePath); + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)完成(不是文件)==="); + return null; + } + + // 4. 校验文件大小(>100bytes视为有效,避免空文件) + if (file.length() <= 100) { + LogUtils.w(TAG, "【checkFileValidity】校验失败:文件过小(无效),路径:" + filePath + ",大小:" + file.length() + "bytes"); + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)完成(文件过小)==="); + return null; + } + + // 所有校验通过,文件有效 + LogUtils.d(TAG, "【checkFileValidity】校验成功:文件有效,路径:" + filePath + ",大小:" + file.length() + "bytes"); + LogUtils.d(TAG, "=== checkFileValidity(文件有效性校验)完成(校验通过)==="); + return file; + } + + /** + * 预览模式兜底:设置默认透明背景(无有效图片时使用,避免显示白色背景) + */ + private void setDefaultBackground() { + LogUtils.d(TAG, "=== setDefaultBackground(设置预览默认透明背景)启动 ==="); + isPreviewMode = true; // 标记为预览模式 + setBackgroundColor(0x00000000); // 当前控件背景全透明 + adjustImageViewSize(); // 调整ImageView尺寸,确保居中且不占位异常 + LogUtils.d(TAG, "【setDefaultBackground】已设置预览默认透明背景"); + LogUtils.d(TAG, "=== setDefaultBackground(设置预览默认透明背景)完成 ==="); + } + + /** + * 核心逻辑:加载图片并设置到ImageView(正式/预览模式通用,统一处理逻辑) + * 负责:路径校验→文件校验→宽高比计算→压缩加载→尺寸调整→显示图片 * @param imagePath 图片绝对路径(可为空) */ private void loadAndSetImageViewBackground(String imagePath) { - // 1. 路径校验(空路径/无效路径 → 显示透明背景) + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)启动 ==="); + // 1. 路径校验:路径为空/空字符串,直接显示默认透明背景 if (imagePath == null || imagePath.isEmpty()) { - LogUtils.e(TAG, "图片路径为空,显示透明背景"); + LogUtils.e(TAG, "【loadAndSetImageViewBackground】加载失败:图片路径为空"); setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)完成(路径为空)==="); return; } - // 2. 文件校验(文件不存在/不是文件 → 显示透明背景) + // 2. 文件校验:文件不存在/不是文件,直接显示默认透明背景 File backgroundFile = new File(imagePath); if (!backgroundFile.exists() || !backgroundFile.isFile()) { - LogUtils.e(TAG, "图片文件不存在或无效:" + imagePath); + LogUtils.e(TAG, "【loadAndSetImageViewBackground】加载失败:文件不存在或无效,路径:" + imagePath); setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)完成(文件无效)==="); return; } - // 3. 计算图片原始宽高比(确保不拉伸) + // 3. 计算图片原始宽高比:计算失败则显示默认透明背景 if (!calculateImageAspectRatio(backgroundFile)) { - LogUtils.e(TAG, "图片尺寸无效,无法加载"); + LogUtils.e(TAG, "【loadAndSetImageViewBackground】加载失败:图片宽高比计算失败,路径:" + imagePath); setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)完成(宽高比计算失败)==="); return; } - // 4. 压缩加载Bitmap(避免OOM,保持原始比例) + // 4. 压缩加载Bitmap:避免OOM,加载失败则显示默认透明背景 Bitmap bitmap = decodeBitmapWithCompress(backgroundFile, 1080, 1920); if (bitmap == null) { - LogUtils.e(TAG, "图片压缩加载失败:" + imagePath); + LogUtils.e(TAG, "【loadAndSetImageViewBackground】加载失败:图片压缩加载失败,路径:" + imagePath); setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)完成(Bitmap加载失败)==="); return; } - // 5. 设置图片到ImageView(保持透明背景) + // 5. 设置图片到ImageView(兼容低版本系统) Drawable backgroundDrawable = new BitmapDrawable(mContext.getResources(), bitmap); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - ivBackground.setBackground(backgroundDrawable); + ivBackground.setBackground(backgroundDrawable); // API 16+ 方法 } else { - ivBackground.setBackgroundDrawable(backgroundDrawable); + ivBackground.setBackgroundDrawable(backgroundDrawable); // 低版本兼容方法 } - // 6. 调整ImageView尺寸(按比例扩展到父控件,居中平铺) + // 6. 调整ImageView尺寸(按图片比例扩展到父控件,确保不拉伸) adjustImageViewSize(); - LogUtils.d(TAG, "图片加载成功(" + (isPreviewMode ? "预览模式" : "正式模式") + ")"); - LogUtils.d(TAG, "图片路径:" + imagePath + ",宽高比:" + imageAspectRatio); + LogUtils.d(TAG, "【loadAndSetImageViewBackground】图片加载成功"); + LogUtils.d(TAG, "→ 当前模式:" + (isPreviewMode ? "预览模式" : "正式模式")); + LogUtils.d(TAG, "→ 图片路径:" + imagePath); + LogUtils.d(TAG, "→ 图片宽高比:" + imageAspectRatio); + LogUtils.d(TAG, "=== loadAndSetImageViewBackground(加载并设置图片)完成(加载成功)==="); } /** - * 【新增工具函数】校验图片路径有效性(路径非空+文件存在+是文件+大小合理) - * 用于: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 图片文件(非空) - * @return 成功:true,失败:false + * 计算图片原始宽高比(宽/高)→ 控制图片不拉伸的核心方法 + * 仅获取图片尺寸,不加载完整图片到内存(节省内存) + * @param file 图片文件(已校验非空) + * @return 计算成功:true,计算失败:false */ private boolean calculateImageAspectRatio(File file) { + LogUtils.d(TAG, "=== calculateImageAspectRatio(计算图片宽高比)启动 ==="); try { BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; // 仅获取尺寸,不加载图片(省内存) + options.inJustDecodeBounds = true; // 仅获取图片尺寸,不加载图片到内存(关键配置) BitmapFactory.decodeFile(file.getAbsolutePath(), options); + // 获取图片原始宽高 int imageWidth = options.outWidth; int imageHeight = options.outHeight; - // 校验尺寸有效性(宽/高必须大于0) + // 校验宽高有效性(宽/高必须大于0,否则视为无效图片) if (imageWidth <= 0 || imageHeight <= 0) { - LogUtils.e(TAG, "图片尺寸无效:宽=" + imageWidth + ", 高=" + imageHeight); + LogUtils.e(TAG, "【calculateImageAspectRatio】计算失败:图片尺寸无效,宽:" + imageWidth + ",高:" + imageHeight); + LogUtils.d(TAG, "=== calculateImageAspectRatio(计算图片宽高比)完成(尺寸无效)==="); return false; } - // 保存原始宽高比(预览/正式模式共用) + // 计算并保存图片原始宽高比(宽/高) imageAspectRatio = (float) imageWidth / imageHeight; - LogUtils.d(TAG, "图片宽高比计算完成:" + imageAspectRatio + "(宽:" + imageWidth + ",高:" + imageHeight + ")"); + LogUtils.d(TAG, "【calculateImageAspectRatio】计算成功"); + LogUtils.d(TAG, "→ 图片原始尺寸:" + imageWidth + "x" + imageHeight); + LogUtils.d(TAG, "→ 图片宽高比:" + imageAspectRatio); + LogUtils.d(TAG, "=== calculateImageAspectRatio(计算图片宽高比)完成(计算成功)==="); return true; } catch (Exception e) { - LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage(), e); + LogUtils.e(TAG, "【calculateImageAspectRatio】计算失败:" + e.getMessage(), e); + LogUtils.d(TAG, "=== calculateImageAspectRatio(计算图片宽高比)完成(发生异常)==="); return false; } } /** - * 调整ImageView尺寸(核心修复:按原始比例扩展到父控件,不拉伸、不裁剪) - * 效果:ImageView按图片比例缩放,最大尺寸填满父控件,同时保持居中 + * 调整ImageView尺寸(核心方法) + * 逻辑:按图片原始宽高比,计算ImageView最大尺寸(填满父控件且不拉伸),并保持居中 + * 适配场景:控件初始化、图片加载后、父容器尺寸变化(如屏幕旋转) */ private void adjustImageViewSize() { - int parentWidth = getWidth(); // 控件宽度(已完全填充父视图) - int parentHeight = getHeight(); // 控件高度(已完全填充父视图) + LogUtils.d(TAG, "=== adjustImageViewSize(调整ImageView尺寸)启动 ==="); + // 获取当前控件尺寸(已完全填充父视图,即父容器尺寸) + int parentWidth = getWidth(); + int parentHeight = getHeight(); - // 若父容器未测量完成(宽度/高度为0),延迟调整(避免尺寸计算错误) + // 父容器未测量完成(宽/高为0),延迟调整(避免尺寸计算错误) if (parentWidth == 0 || parentHeight == 0) { + LogUtils.w(TAG, "【adjustImageViewSize】父容器未测量完成,延迟调整尺寸"); post(new Runnable() { @Override public void run() { - adjustImageViewSize(); + adjustImageViewSize(); // 延迟后重新调整 } }); + LogUtils.d(TAG, "=== adjustImageViewSize(调整ImageView尺寸)完成(延迟调整)==="); return; } + // 初始化ImageView目标宽高 int imageViewWidth, imageViewHeight; - // 核心逻辑:按图片原始比例,计算ImageView最大尺寸(填满父控件,不拉伸) - if (imageAspectRatio >= 1.0f) { // 横图(宽 ≥ 高):优先填满父控件宽度,高度按比例计算 - imageViewWidth = parentWidth; // 宽度=父控件宽度(填满) + + // 核心逻辑:按图片宽高比计算ImageView尺寸(确保不拉伸) + if (imageAspectRatio >= 1.0f) { // 横图(宽 ≥ 高):优先填满父控件宽度 + imageViewWidth = parentWidth; // ImageView宽度 = 父控件宽度(填满) imageViewHeight = Math.round(imageViewWidth / imageAspectRatio); // 高度按比例计算 - // 若高度超过父控件,按父控件高度重新计算(确保完全显示在父控件内) + // 若计算出的高度超过父控件高度,按父控件高度重新计算(确保完全显示在父控件内) if (imageViewHeight > parentHeight) { imageViewHeight = parentHeight; imageViewWidth = Math.round(imageViewHeight * imageAspectRatio); } - } else { // 竖图(宽 < 高):优先填满父控件高度,宽度按比例计算 - imageViewHeight = parentHeight; // 高度=父控件高度(填满) + } else { // 竖图(宽 < 高):优先填满父控件高度 + imageViewHeight = parentHeight; // ImageView高度 = 父控件高度(填满) imageViewWidth = Math.round(imageViewHeight * imageAspectRatio); // 宽度按比例计算 - // 若宽度超过父控件,按父控件宽度重新计算 + // 若计算出的宽度超过父控件宽度,按父控件宽度重新计算 if (imageViewWidth > parentWidth) { imageViewWidth = parentWidth; imageViewHeight = Math.round(imageViewWidth / imageAspectRatio); } } - // 应用尺寸到ImageView(更新布局参数) + // 应用计算出的尺寸到ImageView(更新布局参数) RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivBackground.getLayoutParams(); layoutParams.width = imageViewWidth; layoutParams.height = imageViewHeight; ivBackground.setLayoutParams(layoutParams); - LogUtils.d(TAG, "ImageView尺寸调整完成(" + (isPreviewMode ? "预览模式" : "正式模式") + "):"); - LogUtils.d(TAG, "→ 控件尺寸:" + parentWidth + "x" + parentHeight); - LogUtils.d(TAG, "→ ImageView尺寸:" + imageViewWidth + "x" + imageViewHeight + "(宽高比:" + imageAspectRatio + ")"); + LogUtils.d(TAG, "【adjustImageViewSize】尺寸调整成功"); + LogUtils.d(TAG, "→ 当前模式:" + (isPreviewMode ? "预览模式" : "正式模式")); + LogUtils.d(TAG, "→ 父控件尺寸:" + parentWidth + "x" + parentHeight); + LogUtils.d(TAG, "→ ImageView调整后尺寸:" + imageViewWidth + "x" + imageViewHeight); + LogUtils.d(TAG, "→ 图片宽高比:" + imageAspectRatio); + LogUtils.d(TAG, "=== adjustImageViewSize(调整ImageView尺寸)完成(调整成功)==="); } /** - * 带压缩的Bitmap解码(仅压缩大小,不改变原始比例,避免OOM) + * 带压缩的Bitmap解码(通用方法) + * 逻辑:仅缩小图片(不放大),保持原始比例,降低内存占用,避免OOM * @param file 图片文件 * @param maxWidth 最大宽度(1080px,适配主流手机屏幕) * @param maxHeight 最大高度(1920px,适配主流手机屏幕) - * @return 压缩后的Bitmap(null表示失败) + * @return 压缩后的Bitmap(null表示解码失败) */ private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) { + LogUtils.d(TAG, "=== decodeBitmapWithCompress(压缩解码Bitmap)启动 ==="); try { BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; // 先获取图片尺寸 + options.inJustDecodeBounds = true; // 先获取图片尺寸,不加载完整图片 BitmapFactory.decodeFile(file.getAbsolutePath(), options); - // 计算压缩比例(仅缩小,不放大,保持比例) - int scaleX = options.outWidth / maxWidth; - int scaleY = options.outHeight / maxHeight; - int inSampleSize = Math.max(scaleX, scaleY); + // 计算压缩比例(仅缩小,不放大,取宽高压缩比例的最大值) + int scaleX = options.outWidth / maxWidth; // 宽度方向压缩比例 + int scaleY = options.outHeight / maxHeight; // 高度方向压缩比例 + int inSampleSize = Math.max(scaleX, scaleY); // 最终压缩比例(取最大值,确保不超过最大尺寸) if (inSampleSize <= 0) { - inSampleSize = 1; // 最小压缩比例为1(不压缩) + inSampleSize = 1; // 压缩比例最小为1(不压缩) } - // 正式解码图片(压缩+省内存配置) - options.inJustDecodeBounds = false; - options.inSampleSize = inSampleSize; - options.inPreferredConfig = Bitmap.Config.RGB_565; // 比ARGB_8888省一半内存 - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + // 正式解码图片(配置压缩参数+省内存参数) + options.inJustDecodeBounds = false; // 允许加载完整图片 + options.inSampleSize = inSampleSize; // 设置压缩比例 + options.inPreferredConfig = Bitmap.Config.RGB_565; // 内存优化:比ARGB_8888省一半内存 + Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options); + + if (bitmap != null) { + LogUtils.d(TAG, "【decodeBitmapWithCompress】压缩解码成功"); + LogUtils.d(TAG, "→ 压缩比例:" + inSampleSize); + LogUtils.d(TAG, "→ 解码后Bitmap尺寸:" + bitmap.getWidth() + "x" + bitmap.getHeight()); + } else { + LogUtils.e(TAG, "【decodeBitmapWithCompress】压缩解码失败:Bitmap为空"); + } + + LogUtils.d(TAG, "=== decodeBitmapWithCompress(压缩解码Bitmap)完成 ==="); + return bitmap; } catch (Exception e) { - LogUtils.e(TAG, "图片压缩加载失败:" + e.getMessage(), e); + LogUtils.e(TAG, "【decodeBitmapWithCompress】压缩解码失败:" + e.getMessage(), e); + LogUtils.d(TAG, "=== decodeBitmapWithCompress(压缩解码Bitmap)完成(发生异常)==="); return null; } } /** - * 设置默认透明背景(图片加载失败/路径无效时兜底) - * 确保:无图片时控件完全透明,不遮挡下层视图 + * 通用兜底:设置默认透明背景(图片加载失败/路径无效时使用) + * 确保:无有效图片时,控件完全透明,不遮挡下层视图 */ private void setDefaultTransparentBackground() { - ivBackground.setBackground(new ColorDrawable(0x00000000)); // 全透明背景 - ivBackground.setImageBitmap(null); // 清空图片 - imageAspectRatio = 1.0f; // 重置宽高比(避免影响下次加载) - adjustImageViewSize(); // 调整尺寸(确保ImageView居中且不占位异常) - LogUtils.d(TAG, "已设置默认透明背景"); + LogUtils.d(TAG, "=== setDefaultTransparentBackground(设置通用默认透明背景)启动 ==="); + ivBackground.setBackground(new ColorDrawable(0x00000000)); // ImageView背景全透明 + ivBackground.setImageBitmap(null); // 清空ImageView的图片 + imageAspectRatio = 1.0f; // 重置宽高比(避免影响下次图片加载) + adjustImageViewSize(); // 调整ImageView尺寸,确保居中且不占位异常 + LogUtils.d(TAG, "【setDefaultTransparentBackground】已设置通用默认透明背景"); + LogUtils.d(TAG, "=== setDefaultTransparentBackground(设置通用默认透明背景)完成 ==="); } + // ====================================== 重写父类方法 ====================================== /** - * 父容器尺寸变化时(如屏幕旋转),重新调整ImageView尺寸 - * 确保:控件尺寸变化后,图片仍保持比例+填满父控件 + * 重写父类方法:父容器尺寸变化时调用(如屏幕旋转、窗口大小调整) + * 作用:确保控件尺寸变化后,图片仍保持比例+填满父控件 + * @param w 新宽度 + * @param h 新高度 + * @param oldw 旧宽度 + * @param oldh 旧高度 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - LogUtils.d(TAG, "控件尺寸变化:旧尺寸=" + oldw + "x" + oldh + ",新尺寸=" + w + "x" + h); - adjustImageViewSize(); // 重新调整ImageView尺寸 - } - - /** - * 【对外提供】判断当前是否处于预览模式 - * 用于:Activity中判断是否需要提交预览背景 - */ - public boolean isPreviewMode() { - return isPreviewMode; + LogUtils.d(TAG, "=== onSizeChanged(控件尺寸变化)启动 ==="); + LogUtils.d(TAG, "【onSizeChanged】尺寸变化:旧尺寸=" + oldw + "x" + oldh + ",新尺寸=" + w + "x" + h); + adjustImageViewSize(); // 重新调整ImageView尺寸,适配新的控件尺寸 + LogUtils.d(TAG, "=== onSizeChanged(控件尺寸变化)完成 ==="); } }