源码整理

This commit is contained in:
2025-12-02 13:09:40 +08:00
parent 637d4577df
commit a997fb01c8

View File

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