添加下载图片预览模块(未调试)

This commit is contained in:
ZhanGSKen
2025-11-19 21:24:35 +08:00
parent f8980446a8
commit 230038f6f3
4 changed files with 474 additions and 74 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Wed Nov 19 12:22:35 GMT 2025
#Wed Nov 19 13:23:38 GMT 2025
stageCount=4
libraryProject=
baseVersion=15.11
publishVersion=15.11.3
buildCount=15
buildCount=19
baseBetaVersion=15.11.4

View File

@@ -1,14 +1,21 @@
package cc.winboll.studio.powerbell.dialogs;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; // AndroidX 对话框
import cc.winboll.studio.powerbell.R;
import androidx.appcompat.app.AlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.ImageDownloader;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -17,12 +24,18 @@ import cc.winboll.studio.libappbase.LogUtils;
* 继承 AndroidX AlertDialog绑定自定义布局 dialog_networkbackground.xml
*/
public class NetworkBackgroundDialog extends AlertDialog {
public static final String TAG = "NetworkBackgroundDialog";
// 控件引用
private TextView tvTitle;
private TextView tvContent;
private Button btnCancel;
private Button btnConfirm;
private Button btnPreview;
private EditText etURL;
//private WebView webViewPreview;
ImageView ivBackgroundPreview;
Context mContext;
// 按钮点击回调接口Java7 接口实现)
public interface OnDialogClickListener {
@@ -49,9 +62,10 @@ public class NetworkBackgroundDialog extends AlertDialog {
* 初始化布局和控件
*/
private void initView() {
mContext = this.getContext();
// 加载自定义布局
View dialogView = LayoutInflater.from(getContext())
.inflate(R.layout.dialog_networkbackground, null);
.inflate(R.layout.dialog_networkbackground, null);
// 设置对话框内容视图
setView(dialogView);
@@ -60,6 +74,10 @@ public class NetworkBackgroundDialog extends AlertDialog {
tvContent = (TextView) dialogView.findViewById(R.id.tv_dialog_content);
btnCancel = (Button) dialogView.findViewById(R.id.btn_cancel);
btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm);
btnPreview = (Button) dialogView.findViewById(R.id.btn_preview);
etURL = (EditText) dialogView.findViewById(R.id.et_url);
//webViewPreview = (WebView) dialogView.findViewById(R.id.webview_preview);
ivBackgroundPreview = (ImageView) findViewById(R.id.iv_background_preview);
// 设置按钮点击事件
setButtonClickListeners();
@@ -71,27 +89,37 @@ public class NetworkBackgroundDialog extends AlertDialog {
private void setButtonClickListeners() {
// 取消按钮:关闭对话框 + 回调外部
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
dismiss(); // 关闭对话框
if (listener != null) {
listener.onCancel();
}
}
});
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "取消按钮点击");
dismiss(); // 关闭对话框
if (listener != null) {
listener.onCancel();
}
}
});
// 确认按钮:关闭对话框 + 回调外部
btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
dismiss(); // 关闭对话框
if (listener != null) {
listener.onConfirm();
}
}
});
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认按钮点击");
dismiss(); // 关闭对话框
if (listener != null) {
listener.onConfirm();
}
}
});
// 图片预览按钮:预览输入框地址图片
btnPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d("NetworkBackgroundDialog", "确认预览点击");
String url = etURL.getText().toString();
ImageDownloader.getInstance(mContext).downloadImage(url, mDownloadCallback);
}
});
}
/**
@@ -118,5 +146,50 @@ public class NetworkBackgroundDialog extends AlertDialog {
public void setOnDialogClickListener(OnDialogClickListener listener) {
this.listener = listener;
}
ImageDownloader.DownloadCallback mDownloadCallback = new ImageDownloader.DownloadCallback(){
@Override
public void onSuccess(String filePath) {
ToastUtils.show(filePath);
LogUtils.d(TAG, filePath);
String imagePath = "/storage/emulated/0/Android/data/cc.winboll.studio.powerbell.beta/files/mmexport1713182627221.png";
//String imagePath = filePath;
FileInputStream fis = null;
try {
fis = new FileInputStream(imagePath);
Drawable drawable = Drawable.createFromStream(fis, null);
// 设置背景
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
ivBackgroundPreview.setBackground(drawable);
} else {
ivBackgroundPreview.setBackgroundDrawable(drawable);
}
} catch (Exception e) {
e.printStackTrace();
//ivBackgroundPreview.setBackgroundResource(R.drawable.default_error); // 异常时显示默认图
ivBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); // 异常时显示默认图
} finally {
// Java7 手动关闭流,避免资源泄漏
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
public void onFailure(String errorMsg) {
ToastUtils.show(errorMsg);
LogUtils.d(TAG, errorMsg);
}
};
}

View File

@@ -0,0 +1,294 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 20:52
* @Describe 图片下载工具类(单例模式)
* 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件
*/
public class ImageDownloader {
public static final String TAG = "ImageDownloader";
// 单例实例
private static ImageDownloader sInstance;
// OkHttp 客户端(全局复用,提升性能)
private OkHttpClient mOkHttpClient;
// 缓存目录:/data/data/应用包名/cache/networkdownload
private File mCacheDir;
// 过期时间7天单位毫秒可按需调整
private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000;
/**
* 私有构造(单例模式禁止外部实例化)
* @param context 上下文(用于获取缓存目录)
*/
private ImageDownloader(Context context) {
// 初始化 OkHttp 客户端(设置超时时间)
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();
// 初始化缓存目录networkdownload
initCacheDir(context);
// 初始化时清理过期文件
clearExpiredFiles();
}
/**
* 单例获取方法(线程安全)
* @param context 上下文(建议使用 Application 上下文避免内存泄漏)
* @return 单例实例
*/
public static synchronized ImageDownloader getInstance(Context context) {
if (sInstance == null) {
// 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏
sInstance = new ImageDownloader(context.getApplicationContext());
}
return sInstance;
}
/**
* 初始化缓存目录:若不存在则创建
*/
private void initCacheDir(Context context) {
// 获取应用内置缓存目录(无需权限)
File cacheRoot = context.getCacheDir();
mCacheDir = new File(cacheRoot, "networkdownload");
// 若目录不存在则创建(包括父目录)
if (!mCacheDir.exists()) {
boolean isCreated = mCacheDir.mkdirs();
if (isCreated) {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath());
} else {
LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败");
}
} else {
LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath());
}
}
/**
* 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件)
*/
private void clearExpiredFiles() {
if (mCacheDir == null || !mCacheDir.exists()) {
return;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件,无需清理");
return;
}
long currentTime = System.currentTimeMillis();
int deleteCount = 0;
// 遍历所有文件,删除过期文件
for (File file : files) {
long lastModifyTime = file.lastModified();
if (currentTime - lastModifyTime > EXPIRE_TIME) {
if (file.delete()) {
deleteCount++;
LogUtils.d("ImageDownloader", "删除过期文件:" + file.getName());
} else {
LogUtils.e("ImageDownloader", "删除过期文件失败:" + file.getName());
}
}
}
LogUtils.d("ImageDownloader", "过期文件清理完成,共删除 " + deleteCount + " 个文件");
}
/**
* 下载网络图片到缓存目录
* @param imageUrl 图片网络链接
* @param callback 下载结果回调(成功/失败)
*/
public void downloadImage(final String imageUrl, final DownloadCallback callback) {
// 校验参数
if (TextUtils.isEmpty(imageUrl)) {
if (callback != null) {
callback.onFailure("图片链接为空");
}
return;
}
if (mCacheDir == null || !mCacheDir.exists()) {
if (callback != null) {
callback.onFailure("缓存目录不存在");
}
return;
}
// 构建 OkHttp 请求
Request request = new Request.Builder()
.url(imageUrl)
.build();
// 异步下载(避免阻塞主线程)
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下载失败,回调主线程
if (callback != null) {
callback.onFailure("下载失败:" + e.getMessage());
}
LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
// 响应失败(如 404、500
if (callback != null) {
callback.onFailure("响应失败:" + response.code());
}
LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code());
return;
}
// 响应成功,写入文件
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = response.body().byteStream();
// 生成 UUID 唯一文件名(保留原文件后缀)
String fileExtension = getFileExtension(imageUrl);
String fileName = UUID.randomUUID().toString() + fileExtension;
File imageFile = new File(mCacheDir, fileName);
// 写入文件
outputStream = new FileOutputStream(imageFile);
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.flush();
// 下载成功,回调主线程并返回文件路径
if (callback != null) {
callback.onSuccess(imageFile.getAbsolutePath());
}
LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath());
} catch (IOException e) {
if (callback != null) {
callback.onFailure("文件写入失败:" + e.getMessage());
}
LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage());
} finally {
// 关闭流Java7 手动关闭,避免资源泄漏)
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 关闭响应体
if (response.body() != null) {
response.body().close();
}
}
}
});
}
/**
* 获取 networkdownload 目录中最后下载的文件(按修改时间排序)
* @return 最后下载的文件路径null 表示无文件)
*/
public String getLastDownloadedFile() {
if (mCacheDir == null || !mCacheDir.exists()) {
LogUtils.e("ImageDownloader", "缓存目录不存在");
return null;
}
File[] files = mCacheDir.listFiles();
if (files == null || files.length == 0) {
LogUtils.d("ImageDownloader", "缓存目录无文件");
return null;
}
// 按最后修改时间降序排序,取第一个即为最新文件
File lastFile = files[0];
for (File file : files) {
if (file.lastModified() > lastFile.lastModified()) {
lastFile = file;
}
}
LogUtils.d("ImageDownloader", "最后下载的文件:" + lastFile.getAbsolutePath());
return lastFile.getAbsolutePath();
}
/**
* 工具方法:从图片链接中提取文件后缀(如 .png、.jpg
* @param imageUrl 图片链接
* @return 文件后缀(含点号,若无法提取则返回 .jpg
*/
private String getFileExtension(String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
return ".jpg";
}
int lastDotIndex = imageUrl.lastIndexOf(".");
int lastSlashIndex = imageUrl.lastIndexOf("/");
// 确保后缀在最后一个斜杠之后且长度合理1-5 个字符)
if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) {
String extension = imageUrl.substring(lastDotIndex);
if (extension.length() <= 5) {
return extension.toLowerCase(); // 统一转为小写
}
}
// 无法提取后缀时,默认使用 .jpg
return ".jpg";
}
/**
* 下载结果回调接口Java7 接口实现)
*/
public interface DownloadCallback {
/**
* 下载成功
* @param filePath 图片保存路径
*/
void onSuccess(String filePath);
/**
* 下载失败
* @param errorMsg 失败原因
*/
void onFailure(String errorMsg);
}
}

View File

@@ -1,60 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="20dp"
android:radius="12dp"> <!-- 圆角背景需Android 5.0+低版本可改用shapedrawable -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="20dp"
android:radius="12dp">
<!-- 标题 -->
<TextView
android:id="@+id/tv_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络后台提示"
android:textSize="18sp"
android:textColor="@android:color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络后台提示"
android:textSize="18sp"
android:textColor="@android:color/black"
android:textStyle="bold"/>
<!-- 内容说明 -->
<TextView
android:id="@+id/tv_dialog_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="应用正在后台使用网络,是否继续允许?"
android:textSize="15sp"
android:textColor="@android:color/darker_gray" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
<!-- 按钮容器(水平排列) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:gravity="end">
<EditText
android:layout_width="0dp"
android:ems="10"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:id="@+id/et_url"/>
<!-- 取消按钮 -->
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small"
android:layout_marginRight="8dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="↻"
android:id="@+id/btn_preview"/>
<!-- 确认按钮 -->
<Button
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="允许"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="12dp"
android:layout_gravity="center_vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:id="@+id/iv_background_preview"/>
</LinearLayout>
<TextView
android:id="@+id/tv_dialog_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="应用正在后台使用网络,是否继续允许?"
android:textSize="15sp"
android:textColor="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small"
android:layout_marginRight="8dp"/>
<Button
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="允许"
android:textSize="14sp"
android:background="@android:drawable/btn_default_small"/>
</LinearLayout>
</LinearLayout>