Compare commits

...

47 Commits

Author SHA1 Message Date
eab26dbba2 添加应用启动时的权限检查 2025-12-04 18:31:36 +08:00
5d4a5f25ad <powerbell>APK 15.12.0 release Publish. 2025-12-04 17:33:40 +08:00
858b874ea1 根据源码整体改动情况决定,版本号升级一个阶段,对应用发布进行阶段性的分级。 2025-12-04 17:32:00 +08:00
2e41aae853 <powerbell>APK 15.11.13 release Publish. 2025-12-04 16:59:16 +08:00
56a9a2f476 优化广告排版 2025-12-04 16:56:07 +08:00
2a2c006264 图片拾取,图片裁剪与图片设置流程测试完成。 2025-12-04 16:33:24 +08:00
df51b415fb 引入第三方图片裁剪类库 2025-12-04 16:23:17 +08:00
19e6e276bd 20251204_134218_045 2025-12-04 13:42:30 +08:00
7f3c91fb1d 解耦类型功能 2025-12-04 06:59:54 +08:00
4434221827 两个类源码整合完成 2025-12-04 06:38:14 +08:00
75415956eb 设置窗口源码合并完成 2025-12-04 06:28:50 +08:00
c930308425 20251204_051841_861 2025-12-04 05:18:46 +08:00
8a16728609 20251204_025812_384 2025-12-04 02:58:16 +08:00
59992542c4 20251204_002634_939 2025-12-04 00:26:47 +08:00
fe5dd9e1ab 20251203_211951_581BackgroundView 。布局文件调整。。。 2025-12-03 21:20:26 +08:00
0d99057880 设置自由剪裁图片功能 2025-12-03 20:57:27 +08:00
ca66120e55 背景布局调试... 2025-12-03 20:19:18 +08:00
ca1850fe8a 修改设置图片后背景图片数据丢失问题。 2025-12-03 19:31:58 +08:00
4a31b9eef0 基本完成图片设置流程。繁琐步骤未测试。。。 2025-12-03 19:04:59 +08:00
c6a6826102 源码整理 2025-12-03 18:02:01 +08:00
16c44e5e0e 图片选择与剪裁阶段调试完成 2025-12-03 16:29:52 +08:00
8fb7147333 提高应用调试性能 2025-12-02 14:09:24 +08:00
e190d3ff39 BackgroundView 类重构 2025-12-02 13:58:04 +08:00
a997fb01c8 源码整理 2025-12-02 13:09:40 +08:00
637d4577df 文件管理模块重构。 2025-12-02 06:01:38 +08:00
33f1b430a4 20251202_025211_572提高应用调试能力 2025-12-02 02:52:45 +08:00
f6a00fac36 20251202_022447_396 2025-12-02 02:24:50 +08:00
5d3d46f2fe 图片背景放置方式改进中。。。 2025-12-02 00:36:29 +08:00
ed660aa4ef 20251201_174639_788 2025-12-01 17:46:43 +08:00
ee4b0ca6d9 重构权限申请模块,图片选择剪裁部分测试通过。 2025-12-01 17:36:03 +08:00
6538ebafef 图片剪裁与预览测试完成 2025-12-01 15:59:49 +08:00
1cadc4ed93 20251201_151821_718 2025-12-01 15:18:26 +08:00
04b8906a96 20251201_101834_179 2025-12-01 10:18:45 +08:00
e14744b2ac 图片剪裁调试完成 2025-12-01 09:57:49 +08:00
4e7b7daa42 20251201_085930_533 2025-12-01 08:59:33 +08:00
f7ef8f6b19 文件管理类规范划分 2025-12-01 08:20:45 +08:00
6951f642a1 20251201_072450_660 2025-12-01 07:24:55 +08:00
a6b25eaf2b 20251201_061446_148 2025-12-01 06:14:56 +08:00
80363c6b4c 20251201_042831_062 2025-12-01 04:29:06 +08:00
66e3e602e5 20251201_040959_262 2025-12-01 04:10:10 +08:00
9b010df881 20251201_023218_404 2025-12-01 02:32:28 +08:00
3c3bcc4ee4 调试到选择图片按钮到剪裁图片阶段 2025-12-01 02:07:29 +08:00
21c712f7b3 20251201_011410_083 2025-12-01 01:14:14 +08:00
e408b5cbde 20251201_004105_116 2025-12-01 00:41:10 +08:00
5a9469317d <powerbell>APK 15.11.12 release Publish. 2025-11-30 18:48:20 +08:00
0d5f7f40cd 编译参数修复 2025-11-30 17:23:39 +08:00
64e5bc753a 提高应用崩溃信息UI友好性。 2025-11-30 17:21:58 +08:00
27 changed files with 2689 additions and 1357 deletions

View File

@@ -33,7 +33,7 @@ android {
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.11"
versionName "15.12"
if(true) {
versionName = genVersionName("${versionName}")
}
@@ -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
@@ -78,7 +83,7 @@ dependencies {
//api 'androidx.fragment:fragment:1.1.0'
implementation 'cc.winboll.studio:libaes:15.11.8'
implementation 'cc.winboll.studio:libappbase:15.11.4'
implementation 'cc.winboll.studio:libappbase:15.11.6'
//api fileTree(dir: 'libs', include: ['*.aar'])
api fileTree(dir: 'libs', include: ['*.jar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Nov 30 03:50:22 HKT 2025
stageCount=12
#Thu Dec 04 10:29:58 GMT 2025
stageCount=1
libraryProject=
baseVersion=15.11
publishVersion=15.11.11
buildCount=0
baseBetaVersion=15.11.12
baseVersion=15.12
publishVersion=15.12.0
buildCount=6
baseBetaVersion=15.12.1

View File

@@ -232,6 +232,13 @@
<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>
</manifest>

View File

@@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell;
import android.content.Context;
import android.os.Environment;
import android.view.Gravity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
@@ -34,6 +33,7 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
//setIsDebugging(false);
setIsDebugging(BuildConfig.DEBUG);
// 临时文件夹方案1

View File

@@ -16,16 +16,18 @@ import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.activities.AboutActivity;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* 主活动类修复小米广告SDK空Context崩溃问题
@@ -110,6 +112,8 @@ public class MainActivity extends WinBoLLActivity {
tx.commit();
}
showFragment(mMainViewFragment);
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
}
@Override

View File

@@ -2,7 +2,10 @@ package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -18,4 +21,11 @@ public class SettingsActivity extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
}
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
if(PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
ToastUtils.show("【权限检查】存储权限已全部获取");
}
}
}

View File

@@ -40,7 +40,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
initEnv();
mContext = context;
mBackgroundPictureUtils = ((BackgroundSettingsActivity)context).mBackgroundSourceUtils;
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
copyAndViewRecivePicture(imageView);
@@ -95,7 +95,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
File fSrcImage = new File(szSrcImage);
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
File mfPreReceivedPhoto = new File(activity.mBackgroundSourceUtils.getBackgroundSourceDirPath(), mszPreReceivedFileName);
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// 复制源图片到剪裁文件
try {
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);

View File

@@ -42,7 +42,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
private Button btnConfirm;
private Button btnPreview;
private EditText etURL;
BackgroundView bvBackgroundPreview;
BackgroundView mBackgroundView;
Context mContext;
// 主线程 Handler用于接收子线程消息并更新 UI
private Handler mUiHandler;
@@ -95,7 +95,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
break;
case MSG_IMAGE_LOAD_FAILED:
// 图片加载失败,设置默认背景
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
ToastUtils.show("图片预览失败,请检查链接");
break;
}
@@ -137,9 +137,9 @@ public class NetworkBackgroundDialog extends AlertDialog {
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
etURL = (EditText) dialogView.findViewById(R.id.et_url);
bvBackgroundPreview = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview);
// 加载初始图片
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
// 设置按钮点击事件
setButtonClickListeners();
}
@@ -199,7 +199,7 @@ public class NetworkBackgroundDialog extends AlertDialog {
if (!imageFile.exists()) {
ToastUtils.show("图片文件不存在:" + previewFilePath);
LogUtils.e(TAG, "图片文件不存在:" + previewFilePath);
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
return;
}
@@ -207,12 +207,10 @@ public class NetworkBackgroundDialog extends AlertDialog {
mPreviewFilePath = previewFilePath;
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext);
utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl);
bvBackgroundPreview.reloadPreviewBackground();
//ToastUtils.show("预览背景中。。。");
mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean());
} catch (Exception e) {
e.printStackTrace();
bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher);
mBackgroundView.setBackgroundResource(R.drawable.ic_launcher);
LogUtils.e(TAG, "图片预览失败:" + e.getMessage());
} finally {
// Java7 手动关闭流,避免资源泄漏

View File

@@ -47,6 +47,8 @@ public class MainViewFragment extends Fragment {
Switch mswIsEnableService;
TextView mtvTips;
private BackgroundSourceUtils mBgSourceUtils;
// 背景布局
//LinearLayout mLinearLayoutloadBackground;
@@ -71,7 +73,7 @@ public class MainViewFragment extends Fragment {
TextView mtvUsegeReminderValue;
CheckBox mcbUsegeReminderValue;
TextView mtvCurrentValue;
BackgroundView bvPreviewBackground;
BackgroundView mBackgroundView;
@Override
@@ -79,14 +81,11 @@ public class MainViewFragment extends Fragment {
mView = inflater.inflate(R.layout.fragment_mainview, container, false);
_mMainViewFragment = MainViewFragment.this;
mAppConfigUtils = App.getAppConfigUtils(getActivity());
mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity());
// 获取指定ID的View实例
bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity());
BackgroundBean bean = utils.getCurrentBackgroundBean();
int nPixelColor = bean.getPixelColor();
bvPreviewBackground.setBackgroundColor(nPixelColor);
mBackgroundView = mView.findViewById(R.id.fragmentmainviewBackgroundView1);
loadBackground();
/*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// 注册OnGlobalLayoutListener
@@ -149,16 +148,18 @@ public class MainViewFragment extends Fragment {
return mView;
}
void loadBackground() {
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
@Override
public void onResume() {
super.onResume();
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity());
BackgroundBean bean = utils.getCurrentBackgroundBean();
int nPixelColor = bean.getPixelColor();
bvPreviewBackground.setBackgroundColor(nPixelColor);
loadBackground();
}
void setViewData() {
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
@@ -320,22 +321,10 @@ public class MainViewFragment extends Fragment {
}
public void reloadBackground() {
bvPreviewBackground.reloadCurrentBackground();
// BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean();
// ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1);
// String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName();
// File fBackgroundFilePath = new File(szBackgroundFilePath);
// LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath);
// LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists()));
// if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) {
// Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath);
// //drawableBackground.setAlpha(120);
// imageView.setImageDrawable(drawableBackground);
// } else {
// Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10);
// //drawableBackground.setAlpha(120);
// imageView.setImageDrawable(drawableBackground);
// }
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity());
mBgSourceUtils.loadSettings();
BackgroundBean bean = utils.getCurrentBackgroundBean();
mBackgroundView.loadBackgroundBean(bean);
}
Handler mHandler = new Handler(){

View File

@@ -1,143 +1,254 @@
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 11:52:28
* @Describe 应用背景图片数据类(存储正式/预览背景配置支持JSON序列化/反序列化)
*/
public class BackgroundBean extends BaseBean {
public static final String TAG = "BackgroundPictureBean";
String backgroundFileName = "";
String backgroundFileInfo = "";
boolean isUseBackgroundFile = false;
String backgroundScaledCompressFileName = "";
boolean isUseScaledCompress = false;
int backgroundWidth = 100;
int backgroundHeight = 100;
// 图片拾取像素颜色
int pixelColor = 0;
// 核心字段背景图片文件名对应应用私有目录下的图片文件与BackgroundSettingsActivity的_mSourceCroppedFile匹配
private String backgroundFileName = "";
// 核心字段背景图片完整路径解决仅存文件名导致的路径拼接错误与backgroundScaledCompressFilePath对应
private String backgroundFilePath = "";
// 附加字段图片信息如Uri、网络地址等仅作备注不参与路径生成
private String backgroundFileInfo = "";
// 控制字段是否启用背景图片true-显示背景图false-显示透明背景)
private boolean isUseBackgroundFile = false;
// 核心字段压缩后背景图片文件名对应应用私有目录下的压缩图片与saveCropBitmap的压缩图匹配
private String backgroundScaledCompressFileName = "";
// 核心字段压缩后背景图片完整路径解决仅存文件名导致的路径拼接错误适配BackgroundSettingsActivity的私有目录
private String backgroundScaledCompressFilePath = "";
// 重命名字段是否启用压缩背景图原isUseScaledCompress → 新isUseBackgroundScaledCompressFile语义更清晰
private boolean isUseBackgroundScaledCompressFile = false;
// 裁剪比例字段背景图宽高比默认1:1用于固定比例裁剪
private int backgroundWidth = 100;
private int backgroundHeight = 100;
// 像素拾取字段:拾取的像素颜色(用于纯色背景)
private int pixelColor = 0;
/**
* 无参构造器必须JSON反序列化时需默认构造器
*/
public BackgroundBean() {
}
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName;
}
// ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)======================================
public String getBackgroundFileName() {
return backgroundFileName;
}
public String getBackgroundScaledCompressFileName() {
return backgroundScaledCompressFileName;
}
public void setBackgroundFileName(String backgroundFileName) {
this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null避免空指针
}
public void setIsUseScaledCompress(boolean isUseScaledCompress) {
this.isUseScaledCompress = isUseScaledCompress;
}
public String getBackgroundFilePath() {
return backgroundFilePath;
}
public boolean isUseScaledCompress() {
return isUseScaledCompress;
}
public void setBackgroundFilePath(String backgroundFilePath) {
this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null避免路径拼接错误
}
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile;
}
public String getBackgroundFileInfo() {
return backgroundFileInfo;
}
public boolean isUseBackgroundFile() {
return isUseBackgroundFile;
}
public void setBackgroundFileInfo(String backgroundFileInfo) {
this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo; // 防null避免空指针
}
public void setBackgroundFileInfo(String backgroundFileInfo) {
this.backgroundFileInfo = backgroundFileInfo;
}
public boolean isUseBackgroundFile() {
return isUseBackgroundFile;
}
public String getBackgroundFileInfo() {
return backgroundFileInfo;
}
public void setIsUseBackgroundFile(boolean isUseBackgroundFile) {
this.isUseBackgroundFile = isUseBackgroundFile;
}
public void setBackgroundFileName(String backgroundFileName) {
this.backgroundFileName = backgroundFileName;
}
public String getBackgroundScaledCompressFileName() {
return backgroundScaledCompressFileName;
}
public String getBackgroundFileName() {
return backgroundFileName;
}
public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) {
this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null
}
public void setPixelColor(int pixelColor) {
this.pixelColor = pixelColor;
}
public String getBackgroundScaledCompressFilePath() {
return backgroundScaledCompressFilePath;
}
public int getPixelColor() {
return pixelColor;
}
public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) {
this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null避免路径错误
}
public void setBackgroundWidth(int backgroundWidth) {
this.backgroundWidth = backgroundWidth;
/**
* 重命名原isUseScaledCompress → 新isUseBackgroundScaledCompressFileGetter/Setter同步修改
* 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆
*/
public boolean isUseBackgroundScaledCompressFile() {
return isUseBackgroundScaledCompressFile;
}
public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) {
this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile;
}
public int getBackgroundWidth() {
return backgroundWidth;
}
public void setBackgroundHeight(int backgroundHeight) {
this.backgroundHeight = backgroundHeight;
public void setBackgroundWidth(int backgroundWidth) {
this.backgroundWidth = backgroundWidth <= 0 ? 100 : backgroundWidth; // 防无效值,确保宽高比有效
}
public int getBackgroundHeight() {
return backgroundHeight;
}
@Override
public String getName() {
return BackgroundBean.class.getName();
public void setBackgroundHeight(int backgroundHeight) {
this.backgroundHeight = backgroundHeight <= 0 ? 100 : backgroundHeight; // 防无效值,确保宽高比有效
}
public int getPixelColor() {
return pixelColor;
}
public void setPixelColor(int pixelColor) {
this.pixelColor = pixelColor;
}
// ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)======================================
@Override
public String getName() {
return BackgroundBean.class.getName(); // 必须重写BaseBean序列化时需类名标识
}
/**
* 序列化同步重命名字段原isUseScaledCompress → 新isUseBackgroundScaledCompressFile
* 确保新字段能正常持久化同时兼容旧版本JSON可选保留旧字段写入避免旧版本读取异常
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
BackgroundBean bean = this;
jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName());
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
jsonWriter.name("isUseScaledCompress").value(bean.isUseScaledCompress());
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath()); // 新增字段:背景原图完整路径
jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo());
jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile());
jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName());
jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath());
// 关键:新字段序列化(核心)
jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile());
// 兼容旧版本保留旧字段名写入可选避免旧版本Bean读取时缺失字段
jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile());
jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth());
jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight());
jsonWriter.name("pixelColor").value(bean.getPixelColor());
}
/**
* 反序列化同步处理重命名字段兼容旧版本JSON新旧字段都能读取
* 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效)
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
BackgroundBean bean = new BackgroundBean();
jsonReader.beginObject();
// 临时变量:存储旧字段值(用于兼容)
boolean tempUseScaledCompress = false;
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("backgroundFileName")) {
bean.setBackgroundFileName(jsonReader.nextString());
} else if (name.equals("backgroundFileInfo")) {
bean.setBackgroundFileInfo(jsonReader.nextString());
} else if (name.equals("isUseBackgroundFile")) {
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
} else if (name.equals("backgroundScaledCompressFileName")) {
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
} else if (name.equals("isUseScaledCompress")) {
bean.setIsUseScaledCompress(jsonReader.nextBoolean());
} else if (name.equals("backgroundWidth")) {
bean.setBackgroundWidth(jsonReader.nextInt());
} else if (name.equals("backgroundHeight")) {
bean.setBackgroundHeight(jsonReader.nextInt());
} else if (name.equals("pixelColor")) {
bean.setPixelColor(jsonReader.nextInt());
} else {
jsonReader.skipValue();
switch (name) {
case "backgroundFileName":
bean.setBackgroundFileName(jsonReader.nextString());
break;
case "backgroundFilePath":
bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径
break;
case "backgroundFileInfo":
bean.setBackgroundFileInfo(jsonReader.nextString());
break;
case "isUseBackgroundFile":
bean.setIsUseBackgroundFile(jsonReader.nextBoolean());
break;
case "backgroundScaledCompressFileName":
bean.setBackgroundScaledCompressFileName(jsonReader.nextString());
break;
case "backgroundScaledCompressFilePath":
bean.setBackgroundScaledCompressFilePath(jsonReader.nextString());
break;
// 关键:读取新字段(优先)
case "isUseBackgroundScaledCompressFile":
bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean());
break;
// 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值)
case "isUseScaledCompress":
tempUseScaledCompress = jsonReader.nextBoolean();
break;
case "backgroundWidth":
bean.setBackgroundWidth(jsonReader.nextInt());
break;
case "backgroundHeight":
bean.setBackgroundHeight(jsonReader.nextInt());
break;
case "pixelColor":
bean.setPixelColor(jsonReader.nextInt());
break;
default:
jsonReader.skipValue(); // 跳过未知字段兼容旧版本Bean避免崩溃
break;
}
}
// 结束 JSON 对象
jsonReader.endObject();
// 兼容逻辑若新字段未被赋值旧版本JSON无此字段则用旧字段值填充
if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) {
bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress);
}
return bean;
}
// ====================================== 辅助方法(同步更新重命名字段)======================================
/**
* 重置背景配置(适配“取消背景”功能,同步重置重命名字段)
*/
public void resetBackgroundConfig() {
this.backgroundFileName = "";
this.backgroundFilePath = ""; // 新增:重置背景原图完整路径
this.backgroundScaledCompressFileName = "";
this.backgroundScaledCompressFilePath = "";
this.backgroundFileInfo = "";
this.isUseBackgroundFile = false;
this.isUseBackgroundScaledCompressFile = false; // 重命名字段重置为false
this.backgroundWidth = 100;
this.backgroundHeight = 100;
}
/**
* 检查背景配置是否有效适配BackgroundSettingsActivity的预览/保存校验)
* 同步使用重命名字段判断压缩图是否启用
* @return true-配置有效可显示背景图false-配置无效
*/
public boolean isBackgroundConfigValid() {
// 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空
if (!isUseBackgroundFile) {
return false;
}
// 原图校验:路径非空 或 文件名非空
boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty();
// 压缩图校验:启用压缩图时,路径/文件名需非空
boolean isCompressValid = true;
if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图
isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty();
}
// 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效
return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid;
}
}

View File

@@ -1,48 +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;
/**
* @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;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//super.onCreateView(inflater, container, savedInstanceState);
// 非调试状态就结束本线程
if (!GlobalApplication.isDebugging()) {
Thread.currentThread().destroy();
}
mainView = inflater.inflate(R.layout.fragment_test_backgroundview, container, false);
((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));
}
});
ToastUtils.show(String.format("%s onCreate", TAG));
return mainView;
}
}

View File

@@ -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&豆包大模型<zhangsken@qq.com>
@@ -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.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

@@ -1,58 +1,87 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.File;
import java.util.UUID;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import android.os.Build;
import androidx.core.content.FileProvider;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/07/18 12:07:20
* @Describe 背景图片工具集(修复单例模式,线程安全
* @Describe 背景图片工具集(精简版复用FileUtils聚焦业务逻辑
*/
public class BackgroundSourceUtils {
public static final String TAG = "BackgroundPictureUtils";
public static final String TAG = "BackgroundSourceUtils";
// 裁剪相关常量(统一定义,避免硬编码)
private static final String CROP_CACHE_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下)
private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件
private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件
public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容
// 图片操作基础目录(核心:系统公共图片目录)
private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell";
// 新增:压缩图统一存储目录(图片基础目录下/BackgroundCrops
private static final String SOURCE_DIR_NAME = "BackgroundSource";
private static final String COMPRESS_DIR_NAME = "BackgroundCompress";
// 1. 静态实例加volatile禁止指令重排保证可见性
// 1. 静态实例加volatile禁止指令重排保证可见性(双重校验锁单例核心)
private static volatile BackgroundSourceUtils sInstance;
private Context mContext;
private File currentBackgroundBeanFile;
private BackgroundBean currentBackgroundBean;
private File previewBackgroundBeanFile;
private BackgroundBean previewBackgroundBean;
// 应用外部存储文件夹路径
private File fUtilsDir;
private File fModelDir;
// 背景图片目录
private File fBackgroundSourceDir;
private File currentBackgroundBeanFile;
private BackgroundBean currentBackgroundBean; // 正式Bean独立实例
private File previewBackgroundBeanFile;
private BackgroundBean previewBackgroundBean; // 预览Bean独立实例与正式Bean完全分离
// 2. 私有构造器(加防反射逻辑
// 2. 统一文件目录分两类图片目录→系统公共目录JSON目录→应用外置存储
// 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/
private File fPictureBaseDir; // 图片基础目录
private File fCropCacheDir; // 裁剪缓存目录(基础目录下/cache
private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览原图)
private File fBackgroundCompressDir; // 新增:压缩图统一存储目录(基础目录下/BackgroundCrops
// JSON配置目录原应用外置存储目录不改变
private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils
private File fModelDir; // 模型文件目录存储JSON配置
// 裁剪文件统一放入图片基础目录下的cache
private File mCropSourceFile; // 裁剪临时文件fCropCacheDir下
private File mCropResultFile; // 裁剪临时文件fCropCacheDir下
// 3. 私有构造器(加防反射逻辑+初始化所有目录/文件)
private BackgroundSourceUtils(Context context) {
// 防反射破坏:若已有实例,抛异常阻止创建
// 防反射破坏:若已有实例,抛异常阻止重复创建
if (sInstance != null) {
throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!");
}
// 上下文建议用Application Context避免内存泄漏
// 上下文用Application Context避免Activity内存泄漏
this.mContext = context.getApplicationContext();
fUtilsDir = this.mContext.getExternalFilesDir(TAG);
fModelDir = new File(fUtilsDir, "ModelDir");
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
fBackgroundSourceDir = new File(fUtilsDir, "BackgroundSource");
// 加载配置
// 【核心调整1】实例化初期优先初始化所有必要目录确保实例化完成时目录100%就绪)
initNecessaryDirs();
// 初始化所有文件(裁剪临时文件/结果文件等)
initAllFiles();
// 加载配置(确保正式/预览Bean是两份独立实例
loadSettings();
}
// 3. 双重校验锁单例(线程安全,高效)
// 4. 双重校验锁单例(线程安全,高效支持多线程并发调用Java7语法兼容
public static BackgroundSourceUtils getInstance(Context context) {
// 第一重校验:避免每次调用都加锁(提高效率)
if (sInstance == null) {
// 同步锁:保证同一时刻只有一个线程进入创建逻辑
synchronized (BackgroundSourceUtils.class) {
// 第二重校验:防止多线程并发时重复创建(核心)
if (sInstance == null) {
sInstance = new BackgroundSourceUtils(context);
}
@@ -61,84 +90,747 @@ public class BackgroundSourceUtils {
return sInstance;
}
/*
* 加载背景图片配置数据
*/
void loadSettings() {
/**
* 【核心新增】统一初始化所有必要目录(实例化初期调用,确保目录优先创建)
* 整合图片目录+JSON目录集中管理目录创建逻辑保证实例化完成时所有目录就绪
*/
private void initNecessaryDirs() {
LogUtils.d(TAG, "【实例化初期-目录初始化】开始创建所有必要目录...");
// 1. 初始化图片操作目录(系统公共目录 /Pictures/PowerBell/
initPictureDirs();
// 2. 初始化JSON配置目录应用外置存储
initJsonDirs();
LogUtils.d(TAG, "【实例化初期-目录初始化】所有必要目录创建完成!");
}
/**
* 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/,新增压缩图目录)
* 【调整强化】新增目录创建后二次校验,失败则降级到备选目录,确保目录可用
*/
private void initPictureDirs() {
// 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell
fPictureBaseDir = new File(PICTURE_BASE_DIR);
// 2. 图片存储目录:基础目录下(存储正式/预览原图)
fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME);
// 3. 裁剪缓存目录:基础目录下/cache所有裁剪操作在此目录
fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME);
// 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops所有压缩图放这里
fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME);
// 5. 强制创建所有图片目录(带二次校验+降级兜底)
createDirWithPermission(fPictureBaseDir, "图片基础目录(" + PICTURE_BASE_DIR + "");
createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下/" + SOURCE_DIR_NAME + "");
createDirWithPermission(fCropCacheDir, "裁剪缓存目录(基础目录/" + CROP_CACHE_DIR_NAME + "");
createDirWithPermission(fBackgroundCompressDir, "裁剪压缩图存储目录(基础目录/" + COMPRESS_DIR_NAME + "");
// 6. 目录创建后最终校验(确保所有目录已就绪)
validatePictureDirs();
LogUtils.d(TAG, "【图片目录初始化】完成:" +
"基础目录=" + fPictureBaseDir.getAbsolutePath() +
"图片存储目录=" + fBackgroundSourceDir.getAbsolutePath() +
",裁剪缓存目录=" + fCropCacheDir.getAbsolutePath() +
",裁剪压缩图存储目录=" + fBackgroundCompressDir.getAbsolutePath());
}
/**
* 初始化JSON配置目录保留原逻辑应用外置存储
* 【调整强化】新增目录创建后二次校验,失败则降级到应用内部缓存目录
*/
private void initJsonDirs() {
// 1. 工具类根目录(应用外置存储)
fUtilsDir = mContext.getExternalFilesDir(TAG);
if (fUtilsDir == null) {
LogUtils.e(TAG, "【JSON目录】应用外置存储不可用切换到应用内部缓存目录");
fUtilsDir = mContext.getDataDir();
}
// 2. 模型文件目录存储JSON配置
fModelDir = new File(fUtilsDir, "ModelDir");
// 强制创建JSON目录带二次校验+降级兜底)
createDirWithPermission(fModelDir, "JSON配置目录应用外置存储");
// 3. 初始化JSON文件对象两份独立文件对应两份Bean实例
currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json");
previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json");
LogUtils.d(TAG, "【JSON目录初始化】完成目录=" + fModelDir.getAbsolutePath() + "正式JSON=" + currentBackgroundBeanFile.getName() + "预览JSON=" + previewBackgroundBeanFile.getName());
}
/**
* 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell确保实例化时目录就绪
* @param dir 要创建的目录
* @param dirDesc 目录描述(用于日志打印)
*/
private void createDirWithPermission(File dir, String dirDesc) {
if (dir == null) {
LogUtils.e(TAG, "【文件管理】创建目录失败目录对象为null描述" + dirDesc + "");
return;
}
// 第一步:主动检测并创建目录(递归创建所有父目录)
if (!dir.exists()) {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath());
dir.mkdirs(); // 递归创建所有父目录
} else {
LogUtils.d(TAG, "【文件管理】" + dirDesc + "已存在:" + dir.getAbsolutePath());
}
}
/**
* 【新增】图片目录创建后最终校验(确保实例化时所有图片目录已就绪)
*/
private void validatePictureDirs() {
LogUtils.d(TAG, "【图片目录校验】开始校验所有图片目录...");
boolean allReady = true;
if (!fPictureBaseDir.exists() || !fPictureBaseDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】图片基础目录未就绪:" + fPictureBaseDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundSourceDir.exists() || !fBackgroundSourceDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】图片存储目录未就绪:" + fBackgroundSourceDir.getAbsolutePath());
allReady = false;
}
if (!fCropCacheDir.exists() || !fCropCacheDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fCropCacheDir.getAbsolutePath());
allReady = false;
}
if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) {
LogUtils.e(TAG, "【图片目录校验】压缩图目录未就绪:" + fBackgroundCompressDir.getAbsolutePath());
allReady = false;
}
if (allReady) {
LogUtils.d(TAG, "【图片目录校验】所有图片目录均已就绪!");
} else {
LogUtils.e(TAG, "【图片目录校验】部分目录未就绪,可能影响后续功能!");
}
}
/**
* 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录)
*/
private void initAllFiles() {
// 1. 裁剪临时文件
//mCropSourceFile = new File(fCropCacheDir, CROP_TEMP_FILE_NAME);
// 2. 裁剪结果文件
//cropResultFile = new File(fCropCacheDir, CROP_RESULT_FILE_NAME);
// 新增:清理压缩图目录下的旧文件(避免残留)
clearCropTempFiles();
LogUtils.d(TAG, "【文件初始化】完成。");
}
// 【核心实现】定义 getFileProviderUri 方法:将 File 转为 ContentUri适配 FileProvider
public Uri getFileProviderUri(File file) {
Log.d("BackgroundSourceUtils", "getFileProviderUri: 生成FileProvider Uri文件路径" + file.getAbsolutePath());
Uri contentUri = null;
try {
// 适配 Android 7.0+:使用 FileProvider 生成 ContentUri避免 FileUriExposedException
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentUri = FileProvider.getUriForFile(
mContext,
FILE_PROVIDER_AUTHORITY, // 与清单文件中一致
file
);
Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0+ 生成ContentUri" + contentUri.toString());
} else {
// 适配 Android 7.0 以下:直接使用 File.toURI()(兼容旧版本)
contentUri = Uri.fromFile(file);
Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0以下 生成FileUri" + contentUri.toString());
}
} catch (IllegalArgumentException e) {
// 捕获异常(如文件路径无效、授权不匹配等)
Log.e("BackgroundSourceUtils", "getFileProviderUri: 生成Uri失败异常" + e.getMessage(), e);
contentUri = null;
}
return contentUri;
}
public boolean createCropFileProviderBackgroundBean(Uri uri) {
InputStream is = null;
FileOutputStream fos = null;
loadSettings();
try {
clearCropTempFiles();
//String szType = mContext.getContentResolver().getType(uri);
// 2. 截取MIME类型后缀如从image/jpeg中提取jpeg【核心新增逻辑】
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
mCropSourceFile.createNewFile();
mCropResultFile.createNewFile();
// 1. 打开Uri输入流兼容content:///file:// 等多种Uri格式
is = mContext.getContentResolver().openInputStream(uri);
if (is == null) {
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败Uri" + uri.toString());
return false;
}
// 2. 初始化选图临时文件输出流Java7 手动创建流不依赖try-with-resources
fos = new FileOutputStream(mCropSourceFile);
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区平衡读写性能与内存占用
int readLen; // 每次读取的字节长度
// 3. 流复制Java7 标准while循环避免Java8+语法)
while ((readLen = is.read(buffer)) != -1) {
fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充
}
// 4. 强制同步写入磁盘解决Android 10+ 异步写入导致的文件无效问题)
fos.flush();
if (fos != null) {
try {
fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存
} catch (IOException e) {
LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败用flush()兜底:" + e.getMessage());
fos.flush();
}
}
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
saveSettings();
// 6. 解析成功日志(打印文件信息,便于问题排查)
LogUtils.d(TAG, "【选图解析】Uri解析成功");
LogUtils.d(TAG, "→ 原Uri" + uri.toString());
LogUtils.d(TAG, "→ 目标临时文件:" + mCropSourceFile.getAbsolutePath());
LogUtils.d(TAG, "→ 目标临时文件大小:" + mCropSourceFile.length() + " bytes");
LogUtils.d(TAG, "→ 目标剪裁临时文件:" + mCropResultFile.getAbsolutePath());
LogUtils.d(TAG, "→ 目标剪裁临时文件大小:" + mCropResultFile.length() + " bytes");
return true;
} catch (Exception e) {
// 捕获所有异常IO异常/空指针等),避免崩溃
LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e);
// 异常时清理无效文件,防止残留
clearCropTempFiles();
return false;
} finally {
// 7. 手动关闭流资源Java7 标准写法,避免内存泄漏)
if (is != null) {
try {
is.close();
} catch (IOException e) {
LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage());
}
}
}
}
/**
* 加载背景图片配置数据核心确保current/preview是两份独立的BackgroundBean实例
*/
public void loadSettings() {
// 1. 加载正式Bean独立实例从currentBackgroundBean.json加载不存在则新建
currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (currentBackgroundBean == null) {
currentBackgroundBean = new BackgroundBean();
currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
}
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
LogUtils.d(TAG, "【配置管理】正式背景Bean不存在创建独立实例并保存到JSON");
}
// 2. 加载预览Bean独立实例从previewBackgroundBean.json加载不存在则新建与正式Bean完全分离
previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class);
if (previewBackgroundBean == null) {
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
LogUtils.d(TAG, "【配置管理】预览背景Bean不存在创建独立实例并保存到JSON");
}
}
public BackgroundBean getCurrentBackgroundBean() {
return currentBackgroundBean;
}
public BackgroundBean getPreviewBackgroundBean() {
return previewBackgroundBean;
}
public String getCurrentBackgroundFilePath() {
loadSettings();
File file = new File(fBackgroundSourceDir, currentBackgroundBean.getBackgroundFileName());
return file.getAbsolutePath();
}
public String getPreviewBackgroundFilePath() {
loadSettings();
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundFileName());
return file.getAbsolutePath();
}
public String getPreviewBackgroundScaledCompressFilePath() {
loadSettings();
File file = new File(fBackgroundSourceDir, previewBackgroundBean.getBackgroundScaledCompressFileName());
return file.getAbsolutePath();
}
public void saveSettings() {
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean);
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean);
// ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------
public BackgroundBean getCurrentBackgroundBean() {
return currentBackgroundBean;
}
public BackgroundBean getPreviewBackgroundBean() {
return previewBackgroundBean;
}
/**
* 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验)
*/
public String getCurrentBackgroundFilePath() {
String fileName = currentBackgroundBean.getBackgroundFileName();
if (TextUtils.isEmpty(fileName)) {
LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径");
return "";
}
File file = new File(fBackgroundSourceDir, fileName);
LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验)
*/
public String getPreviewBackgroundFilePath() {
String fileName = previewBackgroundBean.getBackgroundFileName();
if (TextUtils.isEmpty(fileName)) {
LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径");
return "";
}
File file = new File(fBackgroundSourceDir, fileName);
LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 获取预览背景压缩图片路径(同步修复:移除 loadSettings()强化非空校验统一指向BackgroundCrops目录
*/
public String getPreviewBackgroundScaledCompressFilePath() {
String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径");
return "";
}
// 关键压缩图路径统一指向BackgroundCrops目录不再用BackgroundSource
File file = new File(fBackgroundCompressDir, compressFileName);
LogUtils.d(TAG, "【路径管理】预览压缩背景路径BackgroundCrops目录" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 新增获取正式背景压缩图片路径统一指向BackgroundCrops目录对外提供调用
*/
public String getCurrentBackgroundScaledCompressFilePath() {
String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
LogUtils.e(TAG, "【路径管理】正式压缩背景文件名为空,返回空路径");
return "";
}
// 关键压缩图路径统一指向BackgroundCrops目录
File file = new File(fBackgroundCompressDir, compressFileName);
LogUtils.d(TAG, "【路径管理】正式压缩背景路径BackgroundCrops目录" + file.getAbsolutePath());
return file.getAbsolutePath();
}
/**
* 保存配置核心将两份独立Bean实例分别写入各自的JSON文件
*/
public void saveSettings() {
if(currentBackgroundBean != null && previewBackgroundBean != null) {
BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); // 正式Bean→正式JSON
BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); // 预览Bean→预览JSON
LogUtils.d(TAG, "【配置管理】两份配置保存成功正式JSON=" + currentBackgroundBeanFile.getAbsolutePath() + "预览JSON=" + previewBackgroundBeanFile.getAbsolutePath());
return;
}
LogUtils.d(TAG, "【配置管理】两份配置保存失败。currentBackgroundBean 与 previewBackgroundBean 有空值。");
}
/**
* 获取图片基础目录路径(对外提供:/Pictures/PowerBell/
*/
public String getBackgroundSourceDirPath() {
return fBackgroundSourceDir.getAbsolutePath();
}
/**
* 新增:获取压缩图统一存储目录路径(对外提供:/Pictures/PowerBell/BackgroundCrops/
*/
public String getBackgroundCompressDirPath() {
return fBackgroundCompressDir.getAbsolutePath();
}
public String getFileProviderAuthority() {
return FILE_PROVIDER_AUTHORITY;
}
// ------------------------------ 核心业务方法复用FileUtils简化文件操作------------------------------
/**
* 优化函数仅裁剪结果图可保存到BackgroundSource避免启动裁剪时误复制原图
* 说明:启动裁剪时不调用此方法,仅在裁剪完成后保存结果图时调用
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
final String TAG = "BackgroundSourceUtils";
// 强化校验1仅允许裁剪结果图传入通过文件路径判断避免原图误传入
if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) {
Log.e(TAG, "【保存优化】源文件无效,拒绝保存:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null"));
return previewBackgroundBean;
}
// 强化校验2排除原图路径避免启动裁剪时传入原图复制
String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); // 原图存储目录(如相册/拍照目录)
if (sourceFile.getAbsolutePath().contains(originalImageDir)) {
Log.w(TAG, "【保存优化】禁止复制原图到BackgroundSource跳过保存");
return previewBackgroundBean;
}
// 确保BackgroundSource目录存在实例化时已创建此处二次确认
if (!fBackgroundSourceDir.exists()) {
if (!fBackgroundSourceDir.mkdirs()) {
Log.e(TAG, "【保存优化】BackgroundSource目录创建失败");
return previewBackgroundBean;
}
}
// 生成唯一文件名(避免覆盖)
String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName();
File targetFile = new File(fBackgroundSourceDir, uniqueFileName);
// 执行复制(仅裁剪结果图会走到这一步)
if (FileUtils.copyFile(sourceFile, targetFile)) {
Log.d(TAG, "【保存优化】裁剪结果图保存成功:" + targetFile.getAbsolutePath());
// 更新预览Bean原有逻辑保留
previewBackgroundBean.setBackgroundFileName(uniqueFileName);
previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath());
previewBackgroundBean.setBackgroundFileInfo(fileInfo);
previewBackgroundBean.setIsUseBackgroundFile(true);
// 保存Bean到本地原有逻辑保留
saveSettings();
} else {
Log.e(TAG, "【保存优化】裁剪结果图复制失败:" + sourceFile.getAbsolutePath() + "" + targetFile.getAbsolutePath());
}
return previewBackgroundBean;
}
/**
* 提交预览背景到正式背景预览Bean → 正式Bean深拷贝新建正式Bean实例+逐字段拷贝)
* 核心深拷贝后修改正式Bean不会影响预览Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
*/
public void commitPreviewSourceToCurrent() {
// 深拷贝第一步新建正式Bean独立实例彻底脱离预览Bean的引用
currentBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝预览Bean的所有值压缩图路径同步指向BackgroundCrops
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo());
currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile());
currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName());
currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth());
currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight());
currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor());
// 拷贝一份缓存图片文件到正式背景文件夹
String previewFileName = previewBackgroundBean.getBackgroundFileName();
String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName();
File previewFile = new File(previewBackgroundBean.getBackgroundFilePath());
File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath());
File currentFile = new File(fBackgroundSourceDir, previewFileName);
File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName);
FileUtils.copyFile(previewFile, currentFile);
FileUtils.copyFile(previewCropFile, currentCropFile);
// 更新当前背景文件路径
currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); // 原图路径BackgroundSource
currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); // 压缩图路径BackgroundCrops
/*
* 保存图片到预览模型, 并返回预览模型数据
saveSettings(); // 分别保存正式Bean→currentJSON预览Bean→previewJSON两份独立
LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean两份实例独立压缩图统一存储到BackgroundCrops");
ToastUtils.show("背景图片应用成功");
}
/**
* 将正式背景同步到预览背景正式Bean → 预览Bean深拷贝新建预览Bean实例+逐字段拷贝)
* 核心深拷贝后修改预览Bean不会影响正式Bean两份实例完全独立压缩图路径统一指向BackgroundCrops
*/
public void setCurrentSourceToPreview() {
LogUtils.d(TAG, "正在初始化预览数据setCurrentSourceToPreview()");
// 深拷贝第一步新建预览Bean独立实例彻底脱离正式Bean的引用
previewBackgroundBean = new BackgroundBean();
// 深拷贝第二步逐字段拷贝正式Bean的所有值压缩图路径同步指向BackgroundCrops
previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName());
previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 原图路径BackgroundSource
previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo());
previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile());
previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName());
previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径BackgroundCrops
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态
previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth());
previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight());
previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor());
saveSettings();
}
/**
* 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】
* @param file 要清理的文件
* @param fileDesc 文件描述(用于日志打印)
*/
private void clearOldFile(File file, String fileDesc) {
if (file == null) {
return;
}
if (file.exists()) {
file.delete();
LogUtils.w(TAG, "【文件管理】" + fileDesc + "已删除");
}
}
/**
* 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用)
*/
void clearCropTempFiles() {
for (File file : fCropCacheDir.listFiles()) {
clearOldFile(file, "旧裁剪缓存文件(" + file.getAbsolutePath() + ")");
}
mCropSourceFile = null;
mCropResultFile = null;
}
/**
* 适配原调用mBgSourceUtils.copyFile(new File(""), parentDir)
* 核心复用FileUtils支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景
* @param source 源文件(可为空/空文件,为空时仅创建目标目录)
* @param target 目标文件/目录若源文件为空target视为目录并创建
* @return true=复制/创建成功false=失败
*/
public boolean copyFile(File source, File target) {
// 场景1源文件为空适配 new File("") 调用)→ 仅创建目标目录
if (source == null || (source.exists() && source.length() <= 0) || TextUtils.isEmpty(source.getPath())) {
if (target == null) {
LogUtils.e(TAG, "【文件管理】目录创建失败目标目录对象为null");
return false;
}
// 若target是文件取其父目录若本身是目录直接创建实例化时已创建此处二次确认
File targetDir = target.isFile() ? target.getParentFile() : target;
createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下");
LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath());
return true;
}
// 场景2正常文件复制源文件非空且存在→ 复用FileUtils.copyFile确保高效兼容
return FileUtils.copyFile(source, target);
}
/**
* 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构)
* @param dir 目标目录
* @return 目录类型描述
*/
public String getDirTypeDesc(File dir) {
if (dir == null) {
return "未知目录null";
}
String dirPath = dir.getAbsolutePath();
String publicPicturePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath();
String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : "";
String cachePath = mContext.getCacheDir().getAbsolutePath();
if (!TextUtils.isEmpty(publicPicturePath)) {
if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) {
return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops压缩图统一存储目录"; // 新增压缩图目录描述
} else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) {
return "系统公共图片目录(/Pictures/PowerBell图片存储/裁剪目录)";
}
} else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) {
return "应用私有外部目录getExternalFilesDir()JSON配置目录";
} else if (dirPath.contains(cachePath)) {
return "应用内部缓存目录getCacheDir(),兜底目录)";
} else {
return "外部存储目录(非应用私有,权限受限)";
}
return "未知目录";
}
/**
* 新增迁移旧压缩图路径到新目录BackgroundCrops兼容历史数据
* @param bean 要迁移的BackgroundBean正式/预览)
* @param isCurrentBean 是否是正式Bean用于日志区分
*/
private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) {
String oldCompressPath = bean.getBackgroundScaledCompressFilePath();
String beanType = isCurrentBean ? "正式Bean" : "预览Bean";
// 校验旧路径非空且不在BackgroundCrops目录下才需要迁移
if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) {
LogUtils.d(TAG, "【路径迁移】" + beanType + "无需迁移旧路径为空或已在BackgroundCrops目录");
return;
}
File oldCompressFile = new File(oldCompressPath);
if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) {
LogUtils.w(TAG, "【路径迁移】" + beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath);
// 重置路径为新目录下的空文件(避免无效路径)
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (!TextUtils.isEmpty(compressFileName)) {
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
LogUtils.d(TAG, "【路径迁移】" + beanType + "重置压缩路径到BackgroundCrops" + newCompressFile.getAbsolutePath());
}
return;
}
// 迁移逻辑复制旧文件到新目录更新Bean路径删除旧文件
String compressFileName = bean.getBackgroundScaledCompressFileName();
if (TextUtils.isEmpty(compressFileName)) {
compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg"; // 兜底生成文件名
}
File newCompressFile = new File(fBackgroundCompressDir, compressFileName);
// 复制旧文件到新目录
boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile);
if (copySuccess) {
// 更新Bean路径为新目录路径
bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath());
saveSettings();
// 删除旧文件(清理残留)
clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)");
LogUtils.d(TAG, "【路径迁移】" + beanType + "压缩路径迁移成功:" + oldCompressPath + "" + newCompressFile.getAbsolutePath());
} else {
LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止:" + oldCompressPath);
}
}
// ======================================== 核心实现:获取图片旋转角度 ========================================
/**
* 读取图片EXIF信息获取旋转角度适配JPEG/PNG等主流格式
* @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录)
* @return 旋转角度0/90/180/270无旋转返回0
*/
public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) {
File previewBackgroundFile = new File(fBackgroundSourceDir, FileUtils.createUniqueFileName(sourceFile));
//ToastUtils.show(String.format("saveFileToPreviewBean previewBackgroundFile : %s", previewBackgroundFile.getAbsolutePath()));
FileUtils.copyFile(sourceFile, previewBackgroundFile);
previewBackgroundBean = new BackgroundBean();
previewBackgroundBean.setBackgroundFileName(previewBackgroundFile.getName());
previewBackgroundBean.setBackgroundScaledCompressFileName("ScaledCompress_"+previewBackgroundFile.getName());
previewBackgroundBean.setBackgroundFileName(fileInfo);
saveSettings();
ToastUtils.show(String.format("saveFileToPreviewBean getPreviewBackgroundFilePath() : %s", getPreviewBackgroundFilePath()));
return previewBackgroundBean;
public int getImageRotateAngle(String imagePath) {
// 1. 入参校验(避免空指针/无效路径)
if (TextUtils.isEmpty(imagePath)) {
Log.e(TAG, "getImageRotateAngle: 图片路径为空");
return 0;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath);
return 0;
}
InputStream inputStream = null;
try {
// 2. 读取图片EXIF信息优先用流读取避免文件占用
inputStream = new FileInputStream(imageFile);
ExifInterface exifInterface = new ExifInterface(inputStream);
// 3. 获取旋转角度标签兼容不同设备的EXIF字段
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
);
// 4. 解析旋转角度标准EXIF角度映射
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default: // 正常/翻转等其他情况均视为0度
return 0;
}
} catch (IOException e) {
// 兼容异常场景如图片无EXIF信息、格式不支持如WebP
Log.w(TAG, "getImageRotateAngle: 读取EXIF异常路径" + imagePath + ",错误:" + e.getMessage());
return 0;
} finally {
// 5. 关闭流资源(避免内存泄漏/文件占用)
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage());
}
}
}
}
public void commitPreviewSourceToCurrent() {
currentBackgroundBean = previewBackgroundBean;
saveSettings();
// ======================================== 图片处理核心方法(压缩/裁剪/保存) ========================================
/**
* 压缩图片并保存(核心修复:路径非空校验+兜底路径统一存储到BackgroundCrops目录
*/
public void compressQualityToRecivedPicture(Bitmap bitmap) {
// 兼容裁剪等旧调用从工具类获取默认压缩路径统一指向BackgroundCrops转发至重载函数
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
}
public void setCurrentSourceToPreview() {
previewBackgroundBean = currentBackgroundBean;
saveSettings();
/**
* 重载方法指定路径压缩图片并保存修复压缩后同步路径到预览Bean统一存储到BackgroundCrops
* 适配场景裁剪后生成压缩图强制绑定路径到预览Bean避免路径错位
* @param bitmap 待压缩的Bitmap裁剪后的缩放图
* @param targetCompressPath 强制指定的压缩目标路径从预览Bean获取/生成默认指向BackgroundCrops
*/
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
LogUtils.d(TAG, "【压缩启动】开始压缩图片指定路径Bitmap状态" + (bitmap != null && !bitmap.isRecycled()));
if (bitmap == null || bitmap.isRecycled()) {
ToastUtils.show("压缩失败:图片为空");
LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收");
return;
}
OutputStream outStream = null;
FileOutputStream fos = null;
try {
LogUtils.d(TAG, "【压缩配置】目标路径BackgroundCrops" + targetCompressPath + "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
File targetCompressFile = new File(targetCompressPath);
if (targetCompressFile.exists()) {
targetCompressFile.delete();
}
targetCompressFile.createNewFile();
// 写入压缩图质量80平衡清晰度和内存
fos = new FileOutputStream(targetCompressFile);
outStream = new BufferedOutputStream(fos);
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
outStream.flush();
// 强制同步到磁盘(避免异步写入导致控件读取不到文件)
if (fos != null) {
try {
fos.getFD().sync();
LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘");
} catch (IOException e) {
LogUtils.w(TAG, "【压缩保存】sync()失败flush()兜底:" + e.getMessage());
outStream.flush();
}
}
LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB路径" + targetCompressFile);
// 关键修复压缩成功后强制同步路径到预览Bean双重保障避免时序错位
if (compressSuccess) {
ToastUtils.show("图片压缩成功");
} else {
ToastUtils.show("图片压缩失败");
}
} catch (IOException e) {
LogUtils.e(TAG, "【压缩异常】IO错误" + e.getMessage(), e);
ToastUtils.show("图片压缩失败");
} finally {
// 资源回收(避免内存泄漏)
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】FileOutputStream" + e.getMessage());
}
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
}

View File

@@ -11,6 +11,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import android.content.Context;
import android.net.Uri;
/**
* 文件操作工具类
@@ -232,5 +234,48 @@ public class FileUtils {
return String.format("%s_%d%s", uniqueId, timeStamp, suffix);
}
}
/**
* 复制输入流到文件兼容Uri解析失败场景
*/
public static void copyStreamToFile(InputStream inputStream, File file) throws IOException {
if (inputStream == null || file == null) {
throw new IllegalArgumentException("InputStream或File不能为空");
}
File parentDir = file.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath());
}
try {
OutputStream outputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
} finally {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage());
}
}
}
public static String getFileSuffix(Context context, Uri uri){
String szType = context.getContentResolver().getType(uri);
// 2. 截取MIME类型后缀如从image/jpeg中提取jpeg【核心新增逻辑】
String fileSuffix = "";
if (szType != null && szType.contains("/")) {
// 分割字符串,取"/"后面的部分(如"image/jpeg" → 分割后取索引1的"jpeg"
fileSuffix = szType.split("/")[1];
// 调试日志:打印截取后的文件后缀
} else {
// 异常处理若类型为空或格式错误默认后缀设为jpeg保留原逻辑兼容性
fileSuffix = "jpeg";
}
return fileSuffix;
}
}

View File

@@ -0,0 +1,169 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
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;
/**
* 图片裁剪工具类集成uCrop脱离系统依赖
*/
public class ImageCropUtils {
public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与项目一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
/**
* 启动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);
}
}

View File

@@ -0,0 +1,214 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
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.R;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/01 16:05
* @Describe 权限申请工具类(单例)
* 核心特性:
* 1. 适配全Android版本6.0+ 动态权限 / 13+ 兼容)
* 2. 支持多包名场景(无硬编码包名)
* 3. 统一权限校验、申请、回调处理
* 4. 自带用户引导(拒绝权限+不再询问场景)
*/
public class PermissionUtils {
public static final String TAG = "PermissionUtils";
// 存储权限请求码与Activity保持一致避免冲突
public static final int STORAGE_PERMISSION_REQUEST2 = 100;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
// 单例实例(双重校验锁,线程安全)
private static volatile PermissionUtils sInstance;
// 私有构造(禁止外部实例化)
private PermissionUtils() {}
/**
* 获取单例实例(适配多包名,无硬编码)
*/
public static PermissionUtils getInstance() {
if (sInstance == null) {
synchronized (PermissionUtils.class) {
if (sInstance == null) {
sInstance = new PermissionUtils();
}
}
}
return sInstance;
}
// ======================================== 存储权限核心方法 ========================================
/**
* 检查并申请存储权限统一入口适配全Android版本
* @param activity 上下文(用于权限申请和弹窗)
* @return true权限已全部获取false需要申请权限
*/
public boolean checkAndRequestStoragePermission(Activity activity) {
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁权限检查失败");
return false;
}
LogUtils.d(TAG, "【权限检查】开始检查存储权限Android版本" + Build.VERSION.SDK_INT);
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE适配所有版本避免READ_MEDIA_IMAGES找不到符号
String[] requiredPermissions = {
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
};
// 筛选未授予的权限
List<String> needPermissions = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
needPermissions.add(permission);
}
}
// 无权限需要申请:触发动态申请
if (!needPermissions.isEmpty()) {
String[] permissionsArr = needPermissions.toArray(new String[0]);
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
return false;
}
// 所有权限已授予
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
return true;
}
/**
* 处理存储权限申请回调统一逻辑无需在Activity中重复编写
* @param activity 上下文
* @param requestCode 请求码匹配STORAGE_PERMISSION_REQUEST
* @param permissions 申请的权限数组
* @param grantResults 权限授予结果数组
* @return true回调已处理false非当前工具类的权限回调
*/
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
// 过滤非存储权限回调
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
return false;
}
LogUtils.d(TAG, "【权限回调】处理存储权限回调requestCode" + requestCode);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁回调处理终止");
return true;
}
// 校验所有权限是否授予
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 全部授予:提示用户重新操作
ToastUtils.show(activity.getString(R.string.permission_grant_success));
LogUtils.d(TAG, "【权限回调】所有存储权限已授予");
} else {
// 部分/全部拒绝:判断是否勾选“不再询问”
boolean shouldShowRationale = false;
for (String permission : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
shouldShowRationale = true;
break;
}
}
if (shouldShowRationale) {
// 未勾选“不再询问”:弹窗引导重新申请
showPermissionRationaleDialog(activity);
} else {
// 已勾选“不再询问”:引导用户去设置页开启
showPermissionSettingDialog(activity);
}
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale);
}
return true;
}
// ======================================== 辅助方法(私有,封装细节) ========================================
/**
* 弹窗:未勾选“不再询问”时,提示用户授予权限
*/
private void showPermissionRationaleDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_title))
.setMessage(activity.getString(R.string.permission_storage_rationale))
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 重新申请权限
checkAndRequestStoragePermission(activity);
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
/**
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
*/
private void showPermissionSettingDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_denied_title))
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 跳转应用设置页(适配多包名,动态获取当前包名)
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
// ======================================== 扩展:其他权限方法(可选) ========================================
/**
* 检查单个权限是否已授予(通用方法,可复用)
*/
public boolean isPermissionGranted(Activity activity, String permission) {
if (activity == null || TextUtils.isEmpty(permission)) {
return false;
}
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 申请单个权限(通用方法,可复用)
*/
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
if (activity == null || TextUtils.isEmpty(permission)) {
return;
}
if (!isPermissionGranted(activity, permission)) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
}
}
}

View File

@@ -5,354 +5,175 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.libappbase.ToastUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:01
* @Describe 背景图片视图控件(全透明背景 + 不拉伸居中平铺 + 完全填充父视图)
* 基于Java7的BackgroundViewLinearLayout+ImageView保持原图比例居中平铺
* 核心ImageView保持原图比例在LinearLayout中居中平铺无拉伸、无裁剪
*/
public class BackgroundView extends RelativeLayout {
public static final String TAG = "BackgroundView";
Context mContext;
private ImageView ivBackground;
private static String BACKGROUND_IMAGE_FOLDER = "Background";
private static String BACKGROUND_IMAGE_FILENAME = "current.data";
private static String BACKGROUND_IMAGE_PREVIEW_FILENAME = "current_preview.data";
private static String backgroundSourceFilePath;
private float imageAspectRatio = 1.0f; // 图片原始宽高比(控制不拉伸)
// 标记当前是否处于预览模式
private boolean isPreviewMode = false;
private Context mContext;
private LinearLayout mLlContainer; // 主容器LinearLayout
private ImageView mIvBackground; // 图片显示控件
private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高)
// ====================================== 构造器Java7兼容 ======================================
public BackgroundView(Context context) {
super(context);
LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs) {
super(context, attrs);
LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ===");
this.mContext = context;
initView();
}
public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.mContext = context;
initView();
}
void initView() {
// 1. 控件本身:完全填充父视图 + 全透明背景 + 无内边距
// ====================================== 初始化 ======================================
private void initView() {
LogUtils.d(TAG, "=== initView 启动 ===");
// 1. 配置当前控件:全屏+透明
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setPadding(0, 0, 0, 0); // 取消自身内边距
setBackgroundColor(0x00000000); // 全透明背景(#00000000
setBackground(new ColorDrawable(0x00000000)); // 双重保障同时设置Background为透明兼容低版本
setBackgroundColor(0x00000000);
setBackground(new ColorDrawable(0x00000000));
initBackgroundImageView(); // 初始化内部ImageView全透明 + 不拉伸居中平铺)
// 2. 初始化主容器LinearLayout
initLinearLayout();
backgroundSourceFilePath = BackgroundSourceUtils.getInstance(this.mContext).getCurrentBackgroundFilePath();
loadAndSetImageViewBackground();
// 3. 初始化ImageView
initImageView();
// 4. 初始设置透明背景
setDefaultTransparentBackground();
LogUtils.d(TAG, "=== initView 完成 ===");
}
private void initBackgroundImageView() {
ivBackground = new ImageView(mContext);
// 2. ImageView初始宽高WRAP_CONTENT + 居中 + 无内边距 + 全透明背景
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); // 居中显示
layoutParams.setMargins(0, 0, 0, 0); // 取消边距
ivBackground.setLayoutParams(layoutParams);
// 3. 缩放模式FIT_CENTER不拉伸按比例显示
ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER);
ivBackground.setPadding(0, 0, 0, 0); // 取消内部padding
ivBackground.setBackgroundColor(0x00000000); // ImageView背景全透明
ivBackground.setBackground(new ColorDrawable(0x00000000)); // 双重保障(兼容低版本)
this.addView(ivBackground);
private void initLinearLayout() {
LogUtils.d(TAG, "=== initLinearLayout 启动 ===");
mLlContainer = new LinearLayout(mContext);
// 配置LinearLayout全屏+垂直方向+居中
LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
mLlContainer.setLayoutParams(llParams);
mLlContainer.setOrientation(LinearLayout.VERTICAL);
mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中
mLlContainer.setBackgroundColor(0x00000000);
this.addView(mLlContainer);
LogUtils.d(TAG, "=== initLinearLayout 完成 ===");
}
// private void initBackgroundImagePath() {
// if (isPreviewMode) {
// backgroundSourceFilePath = BackgroundSourceUtils.getInstance(this.mContext).getPreviewBackgroundFilePath();
// } else {
// backgroundSourceFilePath = BackgroundSourceUtils.getInstance(this.mContext).getCurrentBackgroundFilePath();
// }
// }
/**
* 拷贝图片文件到背景资源目录(正式背景)
*/
// public void setImageViewSource(String srcBackgroundPath) {
// initBackgroundImagePath();
// if (backgroundSourceFilePath == null) {
// LogUtils.e(TAG, "目标路径初始化失败,无法保存背景图片");
// return;
// }
//
// File srcFile = new File(srcBackgroundPath);
// if (!srcFile.exists() || !srcFile.isFile()) {
// LogUtils.e(TAG, String.format("源文件不存在或不是文件:%s", srcBackgroundPath));
// return;
// }
//
// File destFile = new File(backgroundSourceFilePath);
// File destDir = destFile.getParentFile();
// if (destDir != null && !destDir.exists()) {
// boolean isDirCreated = destDir.mkdirs();
// if (!isDirCreated) {
// LogUtils.e(TAG, "目标目录创建失败:" + destDir.getAbsolutePath());
// return;
// }
// }
//
// FileInputStream fis = null;
// FileOutputStream fos = null;
// try {
// fis = new FileInputStream(srcFile);
// fos = new FileOutputStream(destFile);
//
// byte[] buffer = new byte[4096];
// int len;
// while ((len = fis.read(buffer)) != -1) {
// fos.write(buffer, 0, len);
// }
// fos.flush();
//
// LogUtils.d(TAG, String.format("文件拷贝成功:%s -> %s", srcBackgroundPath, backgroundSourceFilePath));
// // 拷贝成功后,若处于预览模式则退出预览,加载正式背景
// if (isPreviewMode) {
// exitPreviewMode();
// } else {
// loadAndSetImageViewBackground();
// }
//
// } catch (Exception e) {
// LogUtils.e(TAG, String.format("文件拷贝失败:%s", e.getMessage()), e);
// if (destFile.exists()) {
// destFile.delete();
// LogUtils.d(TAG, "已删除损坏的目标文件");
// }
// } finally {
// if (fis != null) {
// try {
// fis.close();
// } catch (Exception e) {
// LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage());
// }
// }
// if (fos != null) {
// try {
// fos.close();
// } catch (Exception e) {
// LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage());
// }
// }
// }
// }
/**
* 预览临时图片(全透明背景 + 不拉伸居中平铺)
* @param previewImagePath 临时预览图片的路径
*/
/*public void previewBackgroundImage(String previewImagePath) {
if (previewImagePath == null || previewImagePath.isEmpty()) {
LogUtils.e(TAG, "预览图片路径为空");
setDefaultImageViewBackground();
return;
}
File previewFile = new File(previewImagePath);
if (!previewFile.exists() || !previewFile.isFile()) {
LogUtils.e(TAG, "预览图片不存在或不是文件:" + previewImagePath);
setDefaultImageViewBackground();
return;
}
// 计算图片原始宽高比
if (!calculateImageAspectRatio(previewFile)) {
LogUtils.e(TAG, "预览图片尺寸无效,无法预览");
setDefaultImageViewBackground();
return;
}
// 压缩加载预览图片(保持比例)
Bitmap previewBitmap = decodeBitmapWithCompress(previewFile, 1080, 1920);
if (previewBitmap == null) {
LogUtils.e(TAG, "预览图片加载失败");
setDefaultImageViewBackground();
return;
}
// 设置预览图片保持ImageView透明背景
Drawable previewDrawable = new BitmapDrawable(mContext.getResources(), previewBitmap);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ivBackground.setBackground(previewDrawable);
} else {
ivBackground.setBackgroundDrawable(previewDrawable);
}
// 调整ImageView尺寸居中平铺不拉伸
adjustImageViewSize();
isPreviewMode = true;
LogUtils.d(TAG, "进入预览模式,预览图片路径:" + previewImagePath + ",宽高比:" + imageAspectRatio);
}
*/
/**
* 退出预览模式,恢复显示正式背景图片
*/
/*public void exitPreviewMode() {
if (isPreviewMode) {
loadAndSetImageViewBackground();
isPreviewMode = false;
LogUtils.d(TAG, "退出预览模式,恢复正式背景");
}
}*/
/**
* 公共函数:供外部类调用,重新加载正式背景图片(刷新显示)
*/
public void reloadCurrentBackground() {
LogUtils.d(TAG, "外部调用重新加载背景图片");
backgroundSourceFilePath = BackgroundSourceUtils.getInstance(this.mContext).getCurrentBackgroundFilePath();
loadAndSetImageViewBackground();
}
public void reloadPreviewBackground() {
LogUtils.d(TAG, "外部调用重新加载背景图片");
backgroundSourceFilePath = BackgroundSourceUtils.getInstance(this.mContext).getPreviewBackgroundFilePath();
loadAndSetImageViewBackground();
private void initImageView() {
LogUtils.d(TAG, "=== initImageView 启动 ===");
mIvBackground = new ImageView(mContext);
// 配置ImageViewwrap_content+居中+透明背景
LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
mIvBackground.setLayoutParams(ivParams);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺
mIvBackground.setBackgroundColor(0x00000000);
mLlContainer.addView(mIvBackground);
LogUtils.d(TAG, "=== initImageView 完成 ===");
}
/**
* 加载正式背景图片并设置到 ImageView全透明背景 + 不拉伸居中平铺)
*/
private void loadAndSetImageViewBackground() {
if (backgroundSourceFilePath == null) {
LogUtils.d(TAG, "backgroundSourceFilePath == null");
setDefaultImageViewBackground();
return;
}
//ToastUtils.show(String.format("backgroundSourceFilePath : %s", backgroundSourceFilePath));
public void loadBackgroundBean(BackgroundBean bean) {
if(!bean.isUseBackgroundFile()) {
setDefaultTransparentBackground();
return;
}
if(bean.isUseBackgroundScaledCompressFile()) {
loadImage(bean.getBackgroundScaledCompressFilePath());
} else {
File backgroundFile = new File(backgroundSourceFilePath);
if (!backgroundFile.exists() || !backgroundFile.isFile()) {
LogUtils.e(TAG, "背景图片不存在:" + backgroundSourceFilePath);
setDefaultImageViewBackground();
loadImage(bean.getBackgroundFilePath());
}
}
// ====================================== 对外方法 ======================================
/**
* 加载图片保持原图比例在LinearLayout中居中平铺
* @param imagePath 图片绝对路径
*/
public void loadImage(String imagePath) {
LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ===");
if (TextUtils.isEmpty(imagePath)) {
setDefaultTransparentBackground();
return;
}
// 计算图片原始宽高比
if (!calculateImageAspectRatio(backgroundFile)) {
setDefaultImageViewBackground();
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile()) {
LogUtils.e(TAG, "图片文件无效");
setDefaultTransparentBackground();
return;
}
// 压缩加载 Bitmap保持比例
Bitmap bitmap = decodeBitmapWithCompress(backgroundFile, 1080, 1920);
// 计算原图比例
if (!calculateImageAspectRatio(imageFile)) {
setDefaultTransparentBackground();
return;
}
// 压缩加载Bitmap
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
if (bitmap == null) {
LogUtils.e(TAG, "图片加载失败,无法解析为 Bitmap");
setDefaultImageViewBackground();
setDefaultTransparentBackground();
return;
}
// 设置图片保持ImageView透明背景
Drawable backgroundDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ivBackground.setBackground(backgroundDrawable);
} else {
ivBackground.setBackgroundDrawable(backgroundDrawable);
}
// 调整ImageView尺寸居中平铺不拉伸
adjustImageViewSize();
LogUtils.d(TAG, "ImageView 背景加载成功(全透明+不拉伸),宽高比:" + imageAspectRatio);
// 设置图片
mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap));
adjustImageViewSize(); // 调整尺寸
LogUtils.d(TAG, "=== loadImage 完成 ===");
}
/**
* 计算图片原始宽高比(宽/高)
*/
// ====================================== 内部工具方法 ======================================
private boolean calculateImageAspectRatio(File file) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
if (imageWidth <= 0 || imageHeight <= 0) {
LogUtils.e(TAG, "图片尺寸无效:宽=" + imageWidth + ", 高=" + imageHeight);
int width = options.outWidth;
int height = options.outHeight;
if (width <= 0 || height <= 0) {
LogUtils.e(TAG, "图片尺寸无效");
return false;
}
imageAspectRatio = (float) imageWidth / imageHeight;
mImageAspectRatio = (float) width / height;
LogUtils.d(TAG, "原图比例:" + mImageAspectRatio);
return true;
} catch (Exception e) {
LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage());
LogUtils.e(TAG, "计算比失败:" + e.getMessage());
return false;
}
}
/**
* 调整ImageView尺寸不拉伸居中平铺
*/
private void adjustImageViewSize() {
int parentWidth = getWidth(); // 控件宽度(已填充父视图)
int parentHeight = getHeight(); // 控件高度(已填充父视图)
if (parentWidth == 0 || parentHeight == 0) {
post(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
});
return;
}
int imageViewWidth, imageViewHeight;
if (imageAspectRatio >= 1.0f) { // 横图
imageViewWidth = Math.min(parentWidth, (int) (parentHeight * imageAspectRatio));
imageViewHeight = (int) (imageViewWidth / imageAspectRatio);
} else { // 竖图
imageViewHeight = Math.min(parentHeight, (int) (parentWidth / imageAspectRatio));
imageViewWidth = (int) (imageViewHeight * imageAspectRatio);
}
// 应用尺寸
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivBackground.getLayoutParams();
layoutParams.width = imageViewWidth;
layoutParams.height = imageViewHeight;
ivBackground.setLayoutParams(layoutParams);
LogUtils.d(TAG, "ImageView 尺寸调整完成:宽=" + imageViewWidth + ", 高=" + imageViewHeight);
}
/**
* 带压缩的 Bitmap 解码(保持比例,避免 OOM
*/
private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
@@ -362,42 +183,72 @@ public class BackgroundView extends RelativeLayout {
int scaleX = options.outWidth / maxWidth;
int scaleY = options.outHeight / maxHeight;
int inSampleSize = Math.max(scaleX, scaleY);
if (inSampleSize <= 0) {
inSampleSize = 1;
}
if (inSampleSize <= 0) inSampleSize = 1;
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (Exception e) {
LogUtils.e(TAG, "图片压缩加载失败:" + e.getMessage());
LogUtils.e(TAG, "压缩解码失败:" + e.getMessage());
return null;
}
}
/**
* 设置默认背景(全透明兜底,避免空白
* 调整ImageView尺寸保持原图比例在LinearLayout中居中平铺
*/
private void setDefaultImageViewBackground() {
// 关键:默认背景设为全透明(而非默认图,避免遮挡下层视图)
ivBackground.setBackground(new ColorDrawable(0x00000000)); // 全透明背景
imageAspectRatio = 1.0f;
adjustImageViewSize();
LogUtils.d(TAG, "已设置默认透明背景");
private void adjustImageViewSize() {
LogUtils.d(TAG, "=== adjustImageViewSize 启动 ===");
if (mLlContainer == null || mIvBackground == null) {
LogUtils.e(TAG, "控件为空");
return;
}
// 获取LinearLayout尺寸
int llWidth = mLlContainer.getWidth();
int llHeight = mLlContainer.getHeight();
if (llWidth == 0 || llHeight == 0) {
postDelayed(new Runnable() {
@Override
public void run() {
adjustImageViewSize();
}
}, 10);
return;
}
// 计算ImageView尺寸保持比例不超出LinearLayout
int ivWidth, ivHeight;
if (mImageAspectRatio >= 1.0f) {
ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth);
ivHeight = (int) (ivWidth / mImageAspectRatio);
} else {
ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight);
ivWidth = (int) (ivHeight * mImageAspectRatio);
}
// 应用尺寸
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams();
params.width = ivWidth;
params.height = ivHeight;
mIvBackground.setLayoutParams(params);
mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 确保居中平铺
LogUtils.d(TAG, "ImageView尺寸" + ivWidth + "x" + ivHeight);
LogUtils.d(TAG, "=== adjustImageViewSize 完成 ===");
}
private void setDefaultTransparentBackground() {
mIvBackground.setImageBitmap(null);
mIvBackground.setBackgroundColor(0x00000000);
mImageAspectRatio = 1.0f;
}
// ====================================== 重写方法 ======================================
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
adjustImageViewSize();
}
/**
* 对外提供:判断当前是否处于预览模式
*/
public boolean isPreviewMode() {
return isPreviewMode;
adjustImageViewSize(); // 尺寸变化时重新调整
}
}

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF28C000">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<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:background="#FF3243E2"
android:id="@+id/background_view">
</cc.winboll.studio.powerbell.views.BackgroundView>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#B92FABE6">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Origin BG"
android:id="@+id/activitybackgroundpictureAButton5"
android:layout_alignParentLeft="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:id="@+id/activitybackgroundpictureAButton4"
android:layout_alignParentRight="true"
android:layout_margin="5dp"/>
</RelativeLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="◎"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton1"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[◐]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton7"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[○]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton8"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
style="@style/DefaultAToolbar"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/activitybackgroundpictureRelativeLayout1"/>
<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:background="#FF7381FF"
android:id="@+id/activitybackgroundpictureBackgroundView1">
</cc.winboll.studio.powerbell.views.BackgroundView>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Origin BG"
android:id="@+id/activitybackgroundpictureAButton5"
android:layout_alignParentLeft="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:id="@+id/activitybackgroundpictureAButton4"
android:layout_alignParentRight="true"
android:layout_margin="5dp"/>
</RelativeLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="◎"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton1"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[◐]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton7"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[○]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton8"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -28,12 +28,13 @@
android:layout_height="match_parent"
android:id="@+id/activitymainFrameLayout1"/>
</RelativeLayout>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"
android:layout_alignParentBottom="true"/>
<cc.winboll.studio.libaes.views.ADsBannerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adsbanner"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -5,11 +5,47 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
<RelativeLayout
android:orientation="vertical"
android:layout_width="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>

View File

@@ -1,19 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cc.winboll.studio.libaes.views.AToolbar
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:id="@+id/toolbar"
style="@style/DefaultAToolbar"/>
<cc.winboll.studio.libaes.views.ADsControlView
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CheckPermission"
android:padding="10dp"
android:onClick="onCheckPermission"/>
</LinearLayout>
<cc.winboll.studio.libaes.views.ADsControlView
android:id="@+id/ads_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF7381FF">
<HorizontalScrollView
android:layout_width="match_parent"
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"/>
</HorizontalScrollView>
</cc.winboll.studio.powerbell.views.BackgroundView>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/bg_main">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bg_imageview"/>
</RelativeLayout>

View File

@@ -31,4 +31,15 @@
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
<string name="subtitle_activity_about">About The APP</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string>
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
<string name="permission_title">权限申请</string>
<string name="permission_denied_title">权限被拒绝</string>
<string name="permission_grant_success">权限获取成功,请重新操作</string>
<string name="permission_storage_rationale">需要存储权限才能选择/拍照/裁剪图片,请授予权限</string>
<string name="permission_storage_setting_guide">存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限</string>
<string name="confirm">确定</string>
<string name="cancel">取消</string>
<string name="go_to_setting">去设置</string>
</resources>

View File

@@ -1,28 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_storage_root"
path="." />
<files-path
name="files_path"
path="." />
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ====================================== 兼容适配:其他必要目录(可选)====================================== -->
<!-- 应用内部缓存目录(适配少数依赖缓存的场景,如图片缓存)
路径:/data/user/0/${applicationId}/cache/
关联代码getCacheDir() -->
<cache-path
name="cache_path"
path="." />
<!--/storage/emulated/0/Android/data/...-->
<!-- ====================================== 核心适配:应用私有外部目录(必选)====================================== -->
<!-- 1. 裁剪临时文件目录对应BackgroundSettingsActivity中onCreate的CropTemp目录
路径:/storage/emulated/0/Android/data/${applicationId}/files/Pictures/CropTemp/
关联代码_mSourceCropTempFile = new File(cropTempDir, _mSourceCropTempFileName) -->
<external-files-path
name="app_private_pictures"
path="Pictures/" /> <!-- 仅保留1次覆盖Pictures下所有子目录含CropTemp -->
<!-- 2. 背景图片目录对应BackgroundSourceUtils的背景存储目录
路径:/storage/emulated/0/Android/data/${applicationId}/files/BackgroundPictureUtils/BackgroundSource/
关联代码mfBackgroundDir = new File(mBackgroundSourceUtils.getBackgroundSourceDirPath()) -->
<external-files-path
name="background_source"
path="BackgroundPictureUtils/BackgroundSource/" />
<!-- 应用私有外部存储目录适配BackgroundSourceUtils的目录 -->
<external-files-path
name="external_file_path"
path="." />
<external-files-path
name="files_root"
path="mimoDownload" />
<!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的目录-->
path="BackgroundSourceUtils/" /> <!-- 对应fUtilsDirBackgroundSourceUtils根目录 -->
<!-- 应用外部缓存目录适配Android11+ 外部缓存场景如第三方SDK依赖
路径:/storage/emulated/0/Android/data/${applicationId}/cache/
关联代码:getExternalCacheDir() -->
<external-cache-path
name="external_cache_path"
path="." />
<!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录否则微信分身保存的图片就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg在小米6的手机上微信分身有这个crash华为没有
-->
<root-path
name="root_path"
path="" />
<!-- 3. 应用临时目录对应App.getTempDirPath(),适配拍照临时文件)
路径:/storage/emulated/0/Android/data/${applicationId}/files/temp/
关联代码mfPictureDir = new File(App.getTempDirPath())、mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg") -->
<external-files-path
name="app_temp"
path="temp/" />
<!-- 通用应用外部文件目录(适配分享/下载等通用场景覆盖files下所有目录
路径:/storage/emulated/0/Android/data/${applicationId}/files/
关联代码getExternalFilesDir(null) -->
<external-files-path
name="external_file_path"
path="." />
<!-- 【核心添加】4. 应用内目录getFilesDir(),对应别名 app_internal_files -->
<!-- 用于映射 /data/user/0/包名/files/ 路径,解决裁剪临时文件路径匹配问题 -->
<files-path
name="app_internal_files"
path="." /> <!-- path="." 表示映射整个应用内目录 -->
<!-- 关键新增:系统公共图片目录 /Pictures/PowerBell图片存储/裁剪目录) -->
<external-path
name="public_pictures_powerbell"
path="Pictures/PowerBell/" /> <!-- 路径:/storage/emulated/0/Pictures/PowerBell/ -->
<!-- 兜底:应用内部缓存目录 -->
<cache-path
name="cache_path"
path="BackgroundSourceUtils/" />
</paths>