From df51b415fbf419268b9ee51c30540e3c54115617 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Thu, 4 Dec 2025 16:23:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5=E7=AC=AC=E4=B8=89=E6=96=B9?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=A3=81=E5=89=AA=E7=B1=BB=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerbell/build.gradle | 7 +- powerbell/build.properties | 4 +- powerbell/src/main/AndroidManifest.xml | 7 + .../BackgroundSettingsActivity.java | 203 ++++++++------ .../unittest/BackgroundViewTestFragment.java | 65 ----- .../unittest/MainUnitTestActivity.java | 176 ++++++++++-- .../utils/BackgroundSourceUtils.java | 25 +- .../powerbell/utils/ImageCropUtils.java | 261 ++++++++++-------- .../main/res/layout/activity_mainunittest.xml | 42 ++- .../layout/fragment_test_backgroundview.xml | 30 -- 10 files changed, 491 insertions(+), 329 deletions(-) delete mode 100644 powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java delete mode 100644 powerbell/src/main/res/layout/fragment_test_backgroundview.xml diff --git a/powerbell/build.gradle b/powerbell/build.gradle index 7a491c55..c1f35a6c 100644 --- a/powerbell/build.gradle +++ b/powerbell/build.gradle @@ -56,7 +56,12 @@ dependencies { api 'com.github.bumptech.glide:glide: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' // SSH diff --git a/powerbell/build.properties b/powerbell/build.properties index bbd31512..d83c9e80 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #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 libraryProject= baseVersion=15.11 publishVersion=15.11.12 -buildCount=205 +buildCount=229 baseBetaVersion=15.11.13 diff --git a/powerbell/src/main/AndroidManifest.xml b/powerbell/src/main/AndroidManifest.xml index 4da18dab..af1dd9ae 100644 --- a/powerbell/src/main/AndroidManifest.xml +++ b/powerbell/src/main/AndroidManifest.xml @@ -232,6 +232,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 f626498e..7d376259 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 @@ -454,6 +454,41 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg ToastUtils.show("拍照图片解析失败"); 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) { LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果"); @@ -658,46 +693,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg }, 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() { initBackgroundViewByPreviewBean(); LogUtils.d(TAG, "【操作回调】取消或失败"); ToastUtils.show("操作已取消"); } - + /** * 启动系统裁剪工具 @@ -708,58 +710,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg * @param isFreeCrop 是否自由裁剪 * @param requestCode 裁剪请求码 */ - 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); - 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()); - } + /** + * 启动系统裁剪工具 + * @param activity 上下文 + * @param srcFile 裁剪原图 + * @param aspectX 裁剪宽比例(自由裁剪传0) + * @param aspectY 裁剪高比例(自由裁剪传0) + * @param isFreeCrop 是否自由裁剪 + * @param requestCode 裁剪请求码 + */ + /** * 获取FileProvider Uri(复用方法,避免重复代码) @@ -788,9 +748,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void finish() { - if(isCommitSettings) { + if (isCommitSettings) { super.finish(); - } else{ + } else { YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener(){ @Override 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()); + } + + } 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 deleted file mode 100644 index 7504073b..00000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java +++ /dev/null @@ -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&豆包大模型 - * @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"); - } -} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java index a5c91248..367785d9 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java @@ -1,14 +1,23 @@ 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.widget.FrameLayout; +import android.os.Environment; +import android.view.View; +import android.widget.Button; import androidx.appcompat.app.AppCompatActivity; -import cc.winboll.studio.libappbase.GlobalApplication; -import cc.winboll.studio.powerbell.R; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import android.nfc.tech.TagTechnology; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.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&豆包大模型 @@ -18,22 +27,157 @@ import cc.winboll.studio.libappbase.ToastUtils; public class MainUnitTestActivity extends AppCompatActivity { 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 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // 非调试状态就退出 - if (!GlobalApplication.isDebugging()) { - finish(); - } + + mBgSourceUtils = BackgroundSourceUtils.getInstance(this); + mBgSourceUtils.loadSettings(); + setContentView(R.layout.activity_mainunittest); - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG); - fragmentTransaction.commit(); + mBackgroundView = findViewById(R.id.backgroundview); + + ((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){ + @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.0(API 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); + } + } } + 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 5979571b..91c4e4c4 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 @@ -245,9 +245,10 @@ public class BackgroundSourceUtils { return contentUri; } - public BackgroundBean createCropFileProviderBackgroundBean(Uri uri) { + public boolean createCropFileProviderBackgroundBean(Uri uri) { InputStream is = null; FileOutputStream fos = null; + loadSettings(); try { clearCropTempFiles(); @@ -265,7 +266,7 @@ public class BackgroundSourceUtils { is = mContext.getContentResolver().openInputStream(uri); if (is == null) { LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败,Uri:" + uri.toString()); - return null; + return false; } // 2. 初始化选图临时文件输出流(Java7 手动创建流,不依赖try-with-resources) @@ -294,7 +295,8 @@ public class BackgroundSourceUtils { previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); - + saveSettings(); + // 6. 解析成功日志(打印文件信息,便于问题排查) LogUtils.d(TAG, "【选图解析】Uri解析成功!"); LogUtils.d(TAG, "→ 原Uri:" + uri.toString()); @@ -302,14 +304,14 @@ public class BackgroundSourceUtils { LogUtils.d(TAG, "→ 目标临时文件大小:" + mCropSourceFile.length() + " bytes"); LogUtils.d(TAG, "→ 目标剪裁临时文件:" + mCropResultFile.getAbsolutePath()); LogUtils.d(TAG, "→ 目标剪裁临时文件大小:" + mCropResultFile.length() + " bytes"); - return previewBackgroundBean; + return true; } catch (Exception e) { // 捕获所有异常(IO异常/空指针等),避免崩溃 LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e); // 异常时清理无效文件,防止残留 clearCropTempFiles(); - return null; + return false; } finally { // 7. 手动关闭流资源(Java7 标准写法,避免内存泄漏) @@ -340,10 +342,7 @@ public class BackgroundSourceUtils { currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建独立实例并保存到JSON"); - } else { - // 修复:加载旧配置时,若压缩图路径不在BackgroundCrops,自动迁移路径(兼容历史数据) - migrateCompressPathToNewDir(currentBackgroundBean, true); - } + } // 2. 加载预览Bean(独立实例:从previewBackgroundBean.json加载,不存在则新建,与正式Bean完全分离) previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); @@ -351,11 +350,7 @@ public class BackgroundSourceUtils { previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); 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 */ public void setCurrentSourceToPreview() { + LogUtils.d(TAG, "正在初始化预览数据,setCurrentSourceToPreview()"); // 深拷贝第一步:新建预览Bean独立实例(彻底脱离正式Bean的引用) previewBackgroundBean = new BackgroundBean(); // 深拷贝第二步:逐字段拷贝正式Bean的所有值(压缩图路径同步指向BackgroundCrops) @@ -561,8 +557,7 @@ public class BackgroundSourceUtils { previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); - saveSettings(); // 分别保存:正式Bean→currentJSON,预览Bean→previewJSON(两份独立) - LogUtils.d(TAG, "【配置管理】正式背景深拷贝到预览Bean:两份实例独立,压缩图统一存储到BackgroundCrops"); + saveSettings(); } /** diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java index 1f28a8c9..d1227b99 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java @@ -1,126 +1,169 @@ package cc.winboll.studio.powerbell.utils; import android.app.Activity; -import android.content.ComponentName; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; -import android.provider.MediaStore; -import android.util.Log; -import android.widget.Toast; import androidx.core.content.FileProvider; 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 com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.UCropActivity; +import java.io.File; +import cc.winboll.studio.powerbell.R; /** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/12/04 06:45 - * @Describe 图片裁剪工具类(仅调用系统裁剪,传入比例/自由裁剪参数) + * 图片裁剪工具类(集成uCrop,脱离系统依赖) */ public class ImageCropUtils { public static final String TAG = "ImageCropUtils"; - // FileProvider授权(与清单文件一致) - - - + // FileProvider 授权(与项目一致) + private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; -// -// /** -// * 保存剪裁后的Bitmap(优化版) -// */ -// private void saveCropBitmap(Bitmap bitmap) { -// if (bitmap == null) { -// ToastUtils.show("剪裁图片为空"); -// return; -// } -// -// // 内存优化:大图片自动缩放 -// Bitmap scaledBitmap = bitmap; -// if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB -// float scale = 1.0f; -// while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { -// scale -= 0.2f; // 每次缩小20% -// if (scale < 0.2f) break; // 最小缩放到20% -// scaledBitmap = scaleBitmap(scaledBitmap, scale); -// } -// if (scaledBitmap != bitmap) { -// bitmap.recycle(); // 回收原Bitmap -// } -// } -// -// // 优化:创建保存目录 -// File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir()); -// if (!backgroundDir.exists()) { -// if (!backgroundDir.mkdirs()) { -// ToastUtils.show("无法创建保存目录"); -// if (scaledBitmap != bitmap) scaledBitmap.recycle(); -// return; -// } -// } -// -// File saveFile = new File(backgroundDir, getBackgroundFileName()); -// -// // 优化:检查文件是否可写 -// if (saveFile.exists() && !saveFile.canWrite()) { -// if (!saveFile.delete()) { -// ToastUtils.show("无法删除旧文件"); -// if (scaledBitmap != bitmap) scaledBitmap.recycle(); -// return; -// } -// } -// -// FileOutputStream fos = null; -// try { -// fos = new FileOutputStream(saveFile); -// boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); -// fos.flush(); -// if (success) { -// ToastUtils.show("保存成功"); -// // 更新数据 -// mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true); -// updatePreviewBackground(); -// } else { -// ToastUtils.show("图片压缩保存失败"); -// } -// } catch (FileNotFoundException e) { -// LogUtils.e(TAG, "文件未找到" + e); -// ToastUtils.show("保存失败:文件路径错误"); -// } catch (IOException e) { -// LogUtils.e(TAG, "写入异常" + e); -// ToastUtils.show("保存失败:磁盘可能已满或路径错误"); -// } finally { -// if (fos != null) { -// try { -// fos.close(); -// } catch (IOException e) { -// LogUtils.e(TAG, "流关闭异常" + e); -// } -// } -// if (scaledBitmap != null && !scaledBitmap.isRecycled()) { -// scaledBitmap.recycle(); -// } -// } -// } -// -// /** -// * 缩放Bitmap -// */ -// private Bitmap scaleBitmap(Bitmap original, float scale) { -// if (original == null) { -// return null; -// } -// int width = (int) (original.getWidth() * scale); -// int height = (int) (original.getHeight() * scale); -// return Bitmap.createScaledBitmap(original, width, height, true); -// } + /** + * 启动uCrop裁剪(核心方法,替代系统裁剪) + * @param activity 上下文 + * @param inputFile 输入图片文件 + * @param outputFile 输出图片文件 + * @param isFreeCrop 是否自由裁剪(true=自由,false=固定比例) + * @param requestCode 裁剪请求码 + */ + public static void startImageCrop(Activity activity, + File inputFile, + File outputFile, + int aspectX, + int aspectY, + boolean isFreeCrop, + int requestCode) { + // 校验输入参数 + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【裁剪异常】上下文Activity无效"); + return; + } + if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { + LogUtils.e(TAG, "【裁剪异常】输入文件无效"); + showToast(activity, "无有效图片可裁剪"); + return; + } + if (outputFile == null) { + LogUtils.e(TAG, "【裁剪异常】输出文件路径为空"); + showToast(activity, "裁剪输出路径无效"); + return; + } + + // 生成输入/输出Uri(适配FileProvider) + Uri inputUri = getFileProviderUri(activity, inputFile); + Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri(兼容低版本) + + // 配置uCrop参数 + UCrop uCrop = UCrop.of(inputUri, outputUri); + UCrop.Options options = new UCrop.Options(); + + // 裁剪模式配置(自由裁剪/固定比例) + if (isFreeCrop) { + // 自由裁剪:无固定比例,可随意调整 + uCrop.withAspectRatio(0, 0); + options.setFreeStyleCropEnabled(true); // 开启自由裁剪 + } else { + // 固定比例(默认1:1,可根据需求修改) + uCrop.withAspectRatio(aspectX, aspectY); + options.setFreeStyleCropEnabled(false); + } + + // 裁剪配置(优化体验) + options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式 + options.setCompressionQuality(100); // 图片质量 + options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面) + options.setToolbarTitle("图片裁剪"); // 工具栏标题 + options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题) + options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色 + + // 应用配置并启动裁剪 + uCrop.withOptions(options); + // 启动uCrop裁剪Activity(替代系统裁剪) + uCrop.start(activity, requestCode); + + LogUtils.d(TAG, "【uCrop启动】成功,输入Uri:" + inputUri + ",输出Uri:" + outputUri + ",请求码:" + requestCode); + } + + /** + * 重载方法:适配BackgroundBean + */ + public static void startImageCrop(Activity activity, + BackgroundBean cropBean, + int aspectX, + int aspectY, + boolean isFreeCrop, + int requestCode) { + File inputFile = new File(cropBean.getBackgroundFilePath()); + File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath()); + startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); + } + + /** + * 生成FileProvider Uri + */ + private static Uri getFileProviderUri(Activity activity, File file) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; + 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); + } } diff --git a/powerbell/src/main/res/layout/activity_mainunittest.xml b/powerbell/src/main/res/layout/activity_mainunittest.xml index b8e7d012..583ef92b 100644 --- a/powerbell/src/main/res/layout/activity_mainunittest.xml +++ b/powerbell/src/main/res/layout/activity_mainunittest.xml @@ -5,11 +5,47 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> - - + android:background="#FF0C6BBF"> + + + + + + + +