引入第三方图片裁剪类库

This commit is contained in:
2025-12-04 16:23:17 +08:00
parent 19e6e276bd
commit df51b415fb
10 changed files with 491 additions and 329 deletions

View File

@@ -56,7 +56,12 @@ dependencies {
api 'com.github.bumptech.glide:glide:4.9.0' api 'com.github.bumptech.glide:glide:4.9.0'
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// uCrop 核心依赖(最新稳定版)
implementation 'com.github.yalantis:ucrop:2.2.8'
// 兼容AndroidX若项目用AndroidX必须添加
//implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
// 应用介绍页类库 // 应用介绍页类库
api 'io.github.medyo:android-about-page:2.0.0' api 'io.github.medyo:android-about-page:2.0.0'
// SSH // SSH

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Thu Dec 04 05:39:23 GMT 2025 #Thu Dec 04 08:21:41 GMT 2025
stageCount=13 stageCount=13
libraryProject= libraryProject=
baseVersion=15.11 baseVersion=15.11
publishVersion=15.11.12 publishVersion=15.11.12
buildCount=205 buildCount=229
baseBetaVersion=15.11.13 baseBetaVersion=15.11.13

View File

@@ -232,6 +232,13 @@
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/> <activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/>
<!-- 1. 注册 UCropActivity关键解决崩溃 -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> <!-- 必须添加Android 12+ 要求显式声明 exported -->
</activity>
</application> </application>
</manifest> </manifest>

View File

@@ -454,6 +454,41 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
ToastUtils.show("拍照图片解析失败"); ToastUtils.show("拍照图片解析失败");
return null; return null;
} }
private void handleSelectPictureResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("图片Uri为空");
return;
}
LogUtils.d(TAG, "【选图回调】Uri : " + selectedImage.toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getContentResolver().takePersistableUriPermission(
selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
}
mBgSourceUtils.createCropFileProviderBackgroundBean(selectedImage);
LogUtils.d(TAG, "【选图同步】路径绑定完成");
// 选图后启动固定比例裁剪(调用工具类)
mBgSourceUtils.loadSettings();
startSystemCrop(
mBgSourceUtils.getPreviewBackgroundBean(),
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
}
private void handleCropImageResult(int requestCode, int resultCode, Intent data) { private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果"); LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果");
@@ -658,46 +693,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}, 50); }, 50);
} }
private void handleSelectPictureResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK || data == null) {
handleOperationCancelOrFail();
return;
}
Uri selectedImage = data.getData();
if (selectedImage == null) {
ToastUtils.show("图片Uri为空");
return;
}
LogUtils.d(TAG, "【选图回调】Uri : " + selectedImage.toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getContentResolver().takePersistableUriPermission(
selectedImage,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
}
BackgroundBean cropBean = mBgSourceUtils.createCropFileProviderBackgroundBean(selectedImage);
LogUtils.d(TAG, "【选图同步】路径绑定完成");
// 选图后启动固定比例裁剪(调用工具类)
startSystemCrop(
cropBean,
mBackgroundView.getWidth(),
mBackgroundView.getHeight(),
false,
REQUEST_CROP_IMAGE
);
}
private void handleOperationCancelOrFail() { private void handleOperationCancelOrFail() {
initBackgroundViewByPreviewBean(); initBackgroundViewByPreviewBean();
LogUtils.d(TAG, "【操作回调】取消或失败"); LogUtils.d(TAG, "【操作回调】取消或失败");
ToastUtils.show("操作已取消"); ToastUtils.show("操作已取消");
} }
/** /**
* 启动系统裁剪工具 * 启动系统裁剪工具
@@ -708,58 +710,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
* @param isFreeCrop 是否自由裁剪 * @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码 * @param requestCode 裁剪请求码
*/ */
public void startSystemCrop(BackgroundBean cropBean, int aspectX, int aspectY, boolean isFreeCrop, int requestCode) { /**
* 启动系统裁剪工具
LogUtils.d(TAG, "startSystemCrop: 启动系统裁剪,自由裁剪:" + isFreeCrop); * @param activity 上下文
File srcFile = new File(cropBean.getBackgroundFilePath()); * @param srcFile 裁剪原图
* @param aspectX 裁剪宽比例自由裁剪传0
// 校验原图 * @param aspectY 裁剪高比例自由裁剪传0
if (srcFile == null || !srcFile.exists() || srcFile.length() <= 100) { * @param isFreeCrop 是否自由裁剪
Toast.makeText(this, "无有效图片可裁剪", Toast.LENGTH_SHORT).show(); * @param requestCode 裁剪请求码
LogUtils.e(TAG, "startSystemCrop: 原图无效"); */
return;
}
// 生成输入Uri
Uri inputUri = getFileProviderUri(srcFile);
if (inputUri == null) {
Toast.makeText(this, "获取图片Uri失败", Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "startSystemCrop: 输入Uri生成失败");
return;
}
// 生成输出文件使用BackgroundSourceUtils的压缩路径
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(this);
File outputFile = new File(bgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
if (outputFile == null) {
Toast.makeText(this, "裁剪输出路径无效", Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "startSystemCrop: 输出文件为空");
return;
}
Uri outputUri = getFileProviderUri(outputFile);
Uri cropOutPutUri = outputUri;
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(inputUri, "image/*");
//intent.setDataAndType(inputUri, activity.getContentResolver().getType(inputUri));
intent.putExtra("crop", "true");
intent.putExtra("noFaceDetection", true);
if (!isFreeCrop) {
intent.putExtra("aspectX", aspectX);
intent.putExtra("aspectY", aspectY);
}
intent.putExtra("return-data", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri);
intent.putExtra("scale", true);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, requestCode);
LogUtils.d(TAG, "startSystemCrop: 启动裁剪成功,输出路径:" + outputFile.getAbsolutePath());
}
/** /**
* 获取FileProvider Uri复用方法避免重复代码 * 获取FileProvider Uri复用方法避免重复代码
@@ -788,9 +748,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
@Override @Override
public void finish() { public void finish() {
if(isCommitSettings) { if (isCommitSettings) {
super.finish(); super.finish();
} else{ } else {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener(){ YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener(){
@Override @Override
public void onYes() { public void onYes() {
@@ -807,5 +767,72 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}); });
} }
} }
public void startSystemCrop(BackgroundBean cropBean, int aspectX, int aspectY, boolean isFreeCrop, int requestCode) {
LogUtils.d(TAG, "startSystemCrop: 启动系统裁剪,自由裁剪:" + isFreeCrop);
File srcFile = new File(cropBean.getBackgroundFilePath());
// 校验原图
if (srcFile == null || !srcFile.exists() || srcFile.length() <= 100) {
Toast.makeText(this, "无有效图片可裁剪", Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "startSystemCrop: 原图无效");
return;
}
// 生成输入Uri
Uri inputUri = getFileProviderUri(srcFile);
if (inputUri == null) {
Toast.makeText(this, "获取图片Uri失败", Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "startSystemCrop: 输入Uri生成失败");
return;
}
// 生成输出文件使用BackgroundSourceUtils的压缩路径
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(this);
bgSourceUtils.loadSettings();
File outputFile = new File(bgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
if (outputFile == null) {
Toast.makeText(this, "裁剪输出路径无效", Toast.LENGTH_SHORT).show();
LogUtils.e(TAG, "startSystemCrop: 输出文件为空");
return;
}
Uri outputUri = getFileProviderUri(outputFile);
Uri cropOutPutUri = outputUri;
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(inputUri, "image/*");
//intent.setDataAndType(inputUri, activity.getContentResolver().getType(inputUri));
intent.putExtra("crop", "true");
intent.putExtra("noFaceDetection", true);
if (!isFreeCrop) {
intent.putExtra("aspectX", aspectX);
intent.putExtra("aspectY", aspectY);
}
// 修复删除return-data=true避免与EXTRA_OUTPUT冲突
// intent.putExtra("return-data", true);
// 修复1明确禁用return-data强制写入文件关键
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri);
intent.putExtra("scale", true);
// 修复2启用缩放补全确保图片适配输出尺寸兼容性
intent.putExtra("scaleUpIfNeeded", true);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// 修复3授予读写权限并添加FLAG_GRANT_PERSISTABLE_URI_PERMISSION适配Android 11+写入权限)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
// 修复4对输出目录授予临时写入权限解决目录无写入权限问题
grantUriPermission("com.android.camera", outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, requestCode);
LogUtils.d(TAG, "startSystemCrop: 启动裁剪成功,输入路径:" + srcFile.getAbsolutePath() + ",输出路径:" + outputFile.getAbsolutePath());
}
} }

View File

@@ -1,65 +0,0 @@
package cc.winboll.studio.powerbell.unittest;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import android.widget.Button;
import cc.winboll.studio.powerbell.MainActivity;
import android.content.Intent;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:16
* @Describe BackgroundViewTestFragment
*/
public class BackgroundViewTestFragment extends Fragment {
public static final String TAG = "BackgroundViewTestFragment";
View mainView;
BackgroundSourceUtils mBgSourceUtils;
BackgroundView mBackgroundView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//super.onCreateView(inflater, container, savedInstanceState);
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
mBgSourceUtils.loadSettings();
mainView = inflater.inflate(R.layout.fragment_test_backgroundview, container, false);
mBackgroundView = mainView.findViewById(R.id.backgroundview);
((Button)mainView.findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}
});
loadBackground();
ToastUtils.show(String.format("%s onCreate", TAG));
return mainView;
}
@Override
public void onResume() {
super.onResume();
loadBackground();
}
void loadBackground() {
mBackgroundView.loadImage("/storage/emulated/0/Pictures/Gallery/owner/素材/1626915857361.png");
}
}

View File

@@ -1,14 +1,23 @@
package cc.winboll.studio.powerbell.unittest; package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.widget.FrameLayout; import android.os.Environment;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.GlobalApplication; import androidx.core.app.ActivityCompat;
import cc.winboll.studio.powerbell.R; import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager; import cc.winboll.studio.libappbase.LogUtils;
import androidx.fragment.app.FragmentTransaction;
import android.nfc.tech.TagTechnology;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -18,22 +27,157 @@ import cc.winboll.studio.libappbase.ToastUtils;
public class MainUnitTestActivity extends AppCompatActivity { public class MainUnitTestActivity extends AppCompatActivity {
public static final String TAG = "MainUnitTestActivity"; public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView;
BackgroundSourceUtils mBgSourceUtils;
BackgroundView mBackgroundView;
// 测试图片路径用Environment获取适配低版本避免硬编码
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// 非调试状态就退出
if (!GlobalApplication.isDebugging()) { mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
finish(); mBgSourceUtils.loadSettings();
}
setContentView(R.layout.activity_mainunittest); setContentView(R.layout.activity_mainunittest);
FragmentManager fragmentManager = getSupportFragmentManager(); mBackgroundView = findViewById(R.id.backgroundview);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG); ((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
fragmentTransaction.commit(); @Override
public void onClick(View v) {
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪测试按钮点击事件(新增权限校验)
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
ToastUtils.show(String.format("%s onCreate", TAG)); ToastUtils.show(String.format("%s onCreate", TAG));
// 加载测试图片(验证图片路径是否有效)
loadBackground();
} }
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
private void startCropTest() {
// 修复2输出路径用Environment获取确保目录存在避免路径无效
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/");
if (!outputDir.exists()) {
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
}
String dstOutputPath = outputDir.getAbsolutePath()
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
// 修复3自由裁剪时比例传0避免100:100过大导致机型崩溃
ImageCropUtils.startImageCrop(
MainUnitTestActivity.this,
new File(szTestSource),
new File(dstOutputPath),
0, // 自由裁剪传0
0, // 自由裁剪传0
true,
REQUEST_CROP_IMAGE
);
}
/**
* 校验存储读写权限适配Android 6.0+ 低版本SDK移除TIRAMISU依赖
*/
private boolean checkStoragePermission() {
// 适配Android 6.0API 23及以上用通用的读写权限移除高版本API
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
/**
* 申请存储读写权限适配低版本SDK移除READ_MEDIA_IMAGES依赖
*/
private void requestStoragePermission() {
LogUtils.d(TAG, "【裁剪测试】申请存储读写权限");
// 用通用的读写权限适配所有Android 6.0+ 机型,无高版本依赖)
ActivityCompat.requestPermissions(
this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION
);
}
/**
* 权限申请回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
// 校验权限是否授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
ToastUtils.show("存储权限已授予,启动裁剪");
startCropTest(); // 权限授予后启动裁剪
} else {
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString()));
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
// 裁剪完成后回收权限
if (requestCode == REQUEST_CROP_IMAGE) {
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
//ImageCropUtils.releaseCropPermission(this, outputUri);
mBackgroundView.loadImage(dstOutputPath);
}
}
void loadBackground() {
// 校验测试图片是否存在(避免路径错误)
File testFile = new File(szTestSource);
if (testFile.exists() && testFile.length() > 100) {
mBackgroundView.loadImage(szTestSource);
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
} else {
ToastUtils.show("测试图片不存在或无效");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
}
}
} }

View File

@@ -245,9 +245,10 @@ public class BackgroundSourceUtils {
return contentUri; return contentUri;
} }
public BackgroundBean createCropFileProviderBackgroundBean(Uri uri) { public boolean createCropFileProviderBackgroundBean(Uri uri) {
InputStream is = null; InputStream is = null;
FileOutputStream fos = null; FileOutputStream fos = null;
loadSettings();
try { try {
clearCropTempFiles(); clearCropTempFiles();
@@ -265,7 +266,7 @@ public class BackgroundSourceUtils {
is = mContext.getContentResolver().openInputStream(uri); is = mContext.getContentResolver().openInputStream(uri);
if (is == null) { if (is == null) {
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败Uri" + uri.toString()); LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败Uri" + uri.toString());
return null; return false;
} }
// 2. 初始化选图临时文件输出流Java7 手动创建流不依赖try-with-resources // 2. 初始化选图临时文件输出流Java7 手动创建流不依赖try-with-resources
@@ -294,7 +295,8 @@ public class BackgroundSourceUtils {
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
saveSettings();
// 6. 解析成功日志(打印文件信息,便于问题排查) // 6. 解析成功日志(打印文件信息,便于问题排查)
LogUtils.d(TAG, "【选图解析】Uri解析成功"); LogUtils.d(TAG, "【选图解析】Uri解析成功");
LogUtils.d(TAG, "→ 原Uri" + uri.toString()); LogUtils.d(TAG, "→ 原Uri" + uri.toString());
@@ -302,14 +304,14 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "→ 目标临时文件大小:" + mCropSourceFile.length() + " bytes"); LogUtils.d(TAG, "→ 目标临时文件大小:" + mCropSourceFile.length() + " bytes");
LogUtils.d(TAG, "→ 目标剪裁临时文件:" + mCropResultFile.getAbsolutePath()); LogUtils.d(TAG, "→ 目标剪裁临时文件:" + mCropResultFile.getAbsolutePath());
LogUtils.d(TAG, "→ 目标剪裁临时文件大小:" + mCropResultFile.length() + " bytes"); LogUtils.d(TAG, "→ 目标剪裁临时文件大小:" + mCropResultFile.length() + " bytes");
return previewBackgroundBean; return true;
} catch (Exception e) { } catch (Exception e) {
// 捕获所有异常IO异常/空指针等),避免崩溃 // 捕获所有异常IO异常/空指针等),避免崩溃
LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e); LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e);
// 异常时清理无效文件,防止残留 // 异常时清理无效文件,防止残留
clearCropTempFiles(); clearCropTempFiles();
return null; return false;
} finally { } finally {
// 7. 手动关闭流资源Java7 标准写法,避免内存泄漏) // 7. 手动关闭流资源Java7 标准写法,避免内存泄漏)
@@ -340,10 +342,7 @@ public class BackgroundSourceUtils {
currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON"); LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON");
} else { }
// 修复加载旧配置时若压缩图路径不在BackgroundCrops自动迁移路径兼容历史数据
migrateCompressPathToNewDir(currentBackgroundBean, true);
}
// 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离 // 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
@@ -351,11 +350,7 @@ public class BackgroundSourceUtils {
previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON"); LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON");
} else {
// 修复加载旧配置时若压缩图路径不在BackgroundCrops自动迁移路径兼容历史数据
migrateCompressPathToNewDir(previewBackgroundBean, false);
} }
LogUtils.d(TAG, "【配置管理】两份Bean实例初始化完成正式Bean=" + currentBackgroundBean.hashCode() + "预览Bean=" + previewBackgroundBean.hashCode() + "hash不同证明实例独立");
} }
// ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------ // ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------
@@ -547,6 +542,7 @@ public class BackgroundSourceUtils {
* 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立压缩图路径统一指向BackgroundCrops * 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
*/ */
public void setCurrentSourceToPreview() { public void setCurrentSourceToPreview() {
LogUtils.d(TAG, "正在初始化预览数据setCurrentSourceToPreview()");
// 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用 // 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用
previewBackgroundBean = new BackgroundBean(); previewBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝正式Bean的所有值压缩图路径同步指向BackgroundCrops // 深拷贝第二步逐字段拷贝正式Bean的所有值压缩图路径同步指向BackgroundCrops
@@ -561,8 +557,7 @@ public class BackgroundSourceUtils {
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立 saveSettings();
LogUtils.d(TAG, "【配置管理】正式背景深拷贝到预览Bean两份实例独立压缩图统一存储到BackgroundCrops");
} }
/** /**

View File

@@ -1,126 +1,169 @@
package cc.winboll.studio.powerbell.utils; package cc.winboll.studio.powerbell.utils;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.model.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import com.yalantis.ucrop.UCropActivity;
import java.io.File;
import cc.winboll.studio.powerbell.R;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 图片裁剪工具类集成uCrop脱离系统依赖
* @Date 2025/12/04 06:45
* @Describe 图片裁剪工具类(仅调用系统裁剪,传入比例/自由裁剪参数)
*/ */
public class ImageCropUtils { public class ImageCropUtils {
public static final String TAG = "ImageCropUtils"; public static final String TAG = "ImageCropUtils";
// FileProvider授权清单文件一致) // FileProvider 授权(与项目一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// /**
// /** * 启动uCrop裁剪核心方法替代系统裁剪
// * 保存剪裁后的Bitmap优化版 * @param activity 上下文
// */ * @param inputFile 输入图片文件
// private void saveCropBitmap(Bitmap bitmap) { * @param outputFile 输出图片文件
// if (bitmap == null) { * @param isFreeCrop 是否自由裁剪true=自由false=固定比例)
// ToastUtils.show("剪裁图片为空"); * @param requestCode 裁剪请求码
// return; */
// } public static void startImageCrop(Activity activity,
// File inputFile,
// // 内存优化:大图片自动缩放 File outputFile,
// Bitmap scaledBitmap = bitmap; int aspectX,
// if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB int aspectY,
// float scale = 1.0f; boolean isFreeCrop,
// while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { int requestCode) {
// scale -= 0.2f; // 每次缩小20% // 校验输入参数
// if (scale < 0.2f) break; // 最小缩放到20% if (activity == null || activity.isFinishing()) {
// scaledBitmap = scaleBitmap(scaledBitmap, scale); LogUtils.e(TAG, "【裁剪异常】上下文Activity无效");
// } return;
// if (scaledBitmap != bitmap) { }
// bitmap.recycle(); // 回收原Bitmap if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
// } LogUtils.e(TAG, "【裁剪异常】输入文件无效");
// } showToast(activity, "无有效图片可裁剪");
// return;
// // 优化:创建保存目录 }
// File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir()); if (outputFile == null) {
// if (!backgroundDir.exists()) { LogUtils.e(TAG, "【裁剪异常】输出文件路径为空");
// if (!backgroundDir.mkdirs()) { showToast(activity, "裁剪输出路径无效");
// ToastUtils.show("无法创建保存目录"); return;
// if (scaledBitmap != bitmap) scaledBitmap.recycle(); }
// return;
// } // 生成输入/输出Uri适配FileProvider
// } Uri inputUri = getFileProviderUri(activity, inputFile);
// Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri兼容低版本
// File saveFile = new File(backgroundDir, getBackgroundFileName());
// // 配置uCrop参数
// // 优化:检查文件是否可写 UCrop uCrop = UCrop.of(inputUri, outputUri);
// if (saveFile.exists() && !saveFile.canWrite()) { UCrop.Options options = new UCrop.Options();
// if (!saveFile.delete()) {
// ToastUtils.show("无法删除旧文件"); // 裁剪模式配置(自由裁剪/固定比例)
// if (scaledBitmap != bitmap) scaledBitmap.recycle(); if (isFreeCrop) {
// return; // 自由裁剪:无固定比例,可随意调整
// } uCrop.withAspectRatio(0, 0);
// } options.setFreeStyleCropEnabled(true); // 开启自由裁剪
// } else {
// FileOutputStream fos = null; // 固定比例默认1:1可根据需求修改
// try { uCrop.withAspectRatio(aspectX, aspectY);
// fos = new FileOutputStream(saveFile); options.setFreeStyleCropEnabled(false);
// boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); }
// fos.flush();
// if (success) { // 裁剪配置(优化体验)
// ToastUtils.show("保存成功"); options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
// // 更新数据 options.setCompressionQuality(100); // 图片质量
// mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true); options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
// updatePreviewBackground(); options.setToolbarTitle("图片裁剪"); // 工具栏标题
// } else { options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
// ToastUtils.show("图片压缩保存失败"); options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// }
// } catch (FileNotFoundException e) { // 应用配置并启动裁剪
// LogUtils.e(TAG, "文件未找到" + e); uCrop.withOptions(options);
// ToastUtils.show("保存失败:文件路径错误"); // 启动uCrop裁剪Activity替代系统裁剪
// } catch (IOException e) { uCrop.start(activity, requestCode);
// LogUtils.e(TAG, "写入异常" + e);
// ToastUtils.show("保存失败:磁盘可能已满或路径错误"); LogUtils.d(TAG, "【uCrop启动】成功输入Uri" + inputUri + "输出Uri" + outputUri + ",请求码:" + requestCode);
// } finally { }
// if (fos != null) {
// try { /**
// fos.close(); * 重载方法适配BackgroundBean
// } catch (IOException e) { */
// LogUtils.e(TAG, "流关闭异常" + e); public static void startImageCrop(Activity activity,
// } BackgroundBean cropBean,
// } int aspectX,
// if (scaledBitmap != null && !scaledBitmap.isRecycled()) { int aspectY,
// scaledBitmap.recycle(); boolean isFreeCrop,
// } int requestCode) {
// } File inputFile = new File(cropBean.getBackgroundFilePath());
// } File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath());
// startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
// /** }
// * 缩放Bitmap
// */ /**
// private Bitmap scaleBitmap(Bitmap original, float scale) { * 生成FileProvider Uri
// if (original == null) { */
// return null; private static Uri getFileProviderUri(Activity activity, File file) {
// } try {
// int width = (int) (original.getWidth() * scale); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// int height = (int) (original.getHeight() * scale); String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
// return Bitmap.createScaledBitmap(original, width, height, true); Uri uri = FileProvider.getUriForFile(activity, authority, file);
// } LogUtils.d(TAG, "【Uri生成】FileProvider Uri" + uri);
return uri;
} else {
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【Uri生成】普通Uri" + uri);
return uri;
}
} catch (Exception e) {
LogUtils.e(TAG, "【Uri生成】失败" + e.getMessage());
return null;
}
}
/**
* 处理uCrop裁剪回调在Activity的onActivityResult中调用
* @param requestCode 请求码
* @param resultCode 结果码
* @param data 回调数据
* @return 裁剪成功返回输出文件路径失败返回null
*/
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
// 校验是否是uCrop的回调
if (requestCode == cropRequestCode) {
if (resultCode == Activity.RESULT_OK && data != null) {
// 裁剪成功获取输出Uri
Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) {
String outputPath = outputUri.getPath();
LogUtils.d(TAG, "【uCrop回调】裁剪成功输出路径" + outputPath);
return outputPath;
}
} else if (resultCode == UCrop.RESULT_ERROR) {
// 裁剪失败,获取异常信息
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "【uCrop回调】裁剪失败" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【uCrop回调】裁剪被取消");
}
}
return null;
}
/**
* 显示Toast
*/
private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
}
}
/**
* 暴露getFileProviderUri方法供外部调用
*/
public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file);
}
} }

View File

@@ -5,11 +5,47 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout <RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/activitymainunittestFrameLayout1"/> android:background="#FF0C6BBF">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/backgroundview"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#AF4FDA4E">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main"
android:id="@+id/btn_main_activity"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TestCropImage"
android:id="@+id/btn_test_cropimage"/>
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF0C6BBF">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/backgroundview"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#AF4FDA4E">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main"
android:id="@+id/btn_main_activity"/>
</HorizontalScrollView>
</RelativeLayout>