------ 图片 / 视频扫描、保存相册、分享文件一站式解决方案
在 Android 项目中,经常会遇到以下需求:
- 获取指定目录下的图片 / 视频文件
- 从系统相册中读取图片或视频
- 下载完成后保存到系统相册
- 分享单个或多个文件(适配 Android 10+)
- 兼容 Scoped Storage(分区存储)
本文将分享一个完整可用、可直接用于生产环境的 MediaFileUtils 工具类。
一、功能概览
本工具类主要实现以下能力:
✅ 扫描指定目录下的图片 / 视频
✅ 获取系统相册中的图片 / 视频
✅ 支持图片、视频分类
✅ 支持文件分享(单个 / 多个)
✅ 支持 Android 10+ 分区存储
✅ 保存文件到系统相册
✅ 支持自定义文件类型 等
二、核心能力总览
| 功能 | 说明 |
|---|---|
| 获取目录下文件 | 支持按图片 / 视频 / 全部筛选 |
| 获取系统相册 | 使用 MediaStore |
| 保存到相册 | 支持 Android Q+ |
| 分享文件 | 支持 FileProvider |
| 多文件分享 | 支持 ACTION_SEND_MULTIPLE |
| 兼容性 | Android 7.0+ |
| 更多 | ... |
三、核心工具类代码
✅ 可直接复制使用
📌 建议类名:MediaFileUtils.java
java
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.text.TextUtils;
import androidx.activity.result.ActivityResultLauncher;
import androidx.core.content.FileProvider;
import com.xxx.xxx.bean.LocalFileNodeBean;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Author: Su
* Date: 2025/12/20
* Description: 工具类:用于获取指定目录下的媒体文件(图片、视频)。
* 功能:
* 1. 获取指定目录下所有文件;
* 2. 可筛选图片、视频或所有文件;
* 3. 可扩展添加其他类型过滤;
*/
public class MediaFileUtils {
/**
* 获取指定路径下所有媒体文件信息(支持过滤类型)
*
* @param context 上下文
* @param basePath 相对路径,如 "MyMedia" 或 "" 获取根路径
* @param mediaType 需要过滤的媒体类型(IMAGE、VIDEO、ALL)
* @return 媒体文件列表(按最后修改时间倒序)
*/
public static List<LocalFileNodeBean> getMediaFileInfos(Context context, String basePath, EnumUtils.MediaFileInfoType mediaType) {
List<LocalFileNodeBean> mediaFiles = new ArrayList<>();
if (context == null) return mediaFiles;
String rootPath = FileUtils.getFilePathDir(context); // 项目中的工具类,返回内部存储根路径
String fullPath = TextUtils.isEmpty(basePath) ? rootPath : rootPath + File.separator + basePath;
File directory = new File(fullPath);
if (!directory.exists() || !directory.isDirectory()) return mediaFiles;
File[] files = directory.listFiles();
if (files == null) return mediaFiles;
for (File file : files) {
if (!file.isFile()) continue;
String name = file.getName().toLowerCase();
if (isImage(name) && (mediaType == EnumUtils.MediaFileInfoType.IMAGE || mediaType == EnumUtils.MediaFileInfoType.ALL)) {
mediaFiles.add(new LocalFileNodeBean(file, EnumUtils.MediaFileInfoType.IMAGE));
} else if (isVideo(name) && (mediaType == EnumUtils.MediaFileInfoType.VIDEO || mediaType == EnumUtils.MediaFileInfoType.ALL)) {
mediaFiles.add(new LocalFileNodeBean(file, EnumUtils.MediaFileInfoType.VIDEO));
}
}
// 按最后修改时间倒序排序
mediaFiles.sort((a, b) -> Long.compare(b.getLastModified(), a.getLastModified()));
return mediaFiles;
}
/**
* 获取系统相册中的所有图片文件信息
*
* @param context 上下文
* @return 图片文件列表(按添加时间倒序)
*/
public static List<LocalFileNodeBean> getAllImageFileInfos(Context context, EnumUtils.MediaFileInfoType mediaType) {
List<LocalFileNodeBean> imageFiles = new ArrayList<>();
if (context == null) return imageFiles;
Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
String[] projection = new String[]{
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.SIZE
};
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
try (Cursor cursor = context.getContentResolver().query(
collection,
projection,
null,
null,
sortOrder)) {
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
int dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED);
int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE);
while (cursor.moveToNext()) {
long id = cursor.getLong(idColumn);
String name = cursor.getString(nameColumn);
long dateAdded = cursor.getLong(dateColumn);
long size = cursor.getLong(sizeColumn);
// 构建内容 Uri
Uri contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
LocalFileNodeBean bean = new LocalFileNodeBean(
contentUri.toString(),
name,
size,
dateAdded * 1000,
mediaType
);
imageFiles.add(bean);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return imageFiles;
}
/**
* 获取系统相册中的所有图片文件信息
*
* @param context 上下文
* @return 图片文件列表(按添加时间倒序)
*/
public List<String> getAllImages(Context context) {
List<String> imagePaths = new ArrayList<>();
String[] projection = new String[]{
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA // 注意:从 Android Q 开始可能废弃
};
Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
try (Cursor cursor = context.getContentResolver().query(
collection,
projection,
null,
null,
MediaStore.Images.Media.DATE_ADDED + " DESC")) {
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
while (cursor.moveToNext()) {
long id = cursor.getLong(idColumn);
String name = cursor.getString(nameColumn);
Uri contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
imagePaths.add(contentUri.toString());
}
}
}
return imagePaths;
}
/**
* 获取指定目录下的媒体文件
*
* @param context 上下文
* @param basePath 相对路径(如 "MyMedia"),将拼接到内部路径后查找
* @param mediaType 要获取的媒体类型(图片、视频、全部)
* @return 媒体文件列表(按最后修改时间倒序)
*/
public static List<File> getMediaFiles(Context context, String basePath, EnumUtils.MediaFileInfoType mediaType) {
List<File> result = new ArrayList<>();
if (context == null || TextUtils.isEmpty(basePath)) return result;
// 拼接完整目录路径
String fullPath = FileUtils.getFilePathDir(context) + File.separator + basePath;
File dir = new File(fullPath);
if (!dir.exists() || !dir.isDirectory()) return result;
// 遍历目录
File[] files = dir.listFiles();
if (files == null || files.length == 0) return result;
for (File file : files) {
if (file.isFile()) {
String name = file.getName().toLowerCase();
if (mediaType == EnumUtils.MediaFileInfoType.ALL ||
(mediaType == EnumUtils.MediaFileInfoType.IMAGE && isImage(name)) ||
(mediaType == EnumUtils.MediaFileInfoType.VIDEO && isVideo(name))) {
result.add(file);
}
}
}
// 按修改时间倒序排序
result.sort((f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
return result;
}
/**
* 判断是否为图片文件
*
* @param fileName 文件名
* @return 是否为图片
*/
private static boolean isImage(String fileName) {
return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") ||
fileName.endsWith(".png") || fileName.endsWith(".bmp") ||
fileName.endsWith(".webp") || fileName.endsWith(".gif");
}
/**
* 判断是否为视频文件
*
* @param fileName 文件名
* @return 是否为视频
*/
private static boolean isVideo(String fileName) {
return fileName.endsWith(".mp4") || fileName.endsWith(".avi") ||
fileName.endsWith(".mov") || fileName.endsWith(".mkv") ||
fileName.endsWith(".flv") || fileName.endsWith(".3gp");
}
/**
* 分享多个文件(带分享面板结果)
*
* @param context 上下文
* @param fileList 要分享的文件列表
* @param launcher ActivityResultLauncher(用于启动系统分享)
*/
public static void shareMultipleFiles(Context context, List<File> fileList,
ActivityResultLauncher<Intent> launcher) {
if (fileList == null || fileList.isEmpty()) return;
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
ArrayList<Uri> uriList = new ArrayList<>();
for (File file : fileList) {
if (file.exists()) {
Uri uri = FileProvider.getUriForFile(
context,
"com.haizhen.smartcarhome.fileprovider",
file
);
uriList.add(uri);
}
}
if (uriList.isEmpty()) return;
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
launcher.launch(Intent.createChooser(intent, "分享文件"));
}
/**
* 分享单个文件(带分享面板结果)
*
* @param context 上下文
* @param file 要分享的文件列表
* @param launcher ActivityResultLauncher(用于启动系统分享)
*/
public static void shareFile(Context context, File file,
ActivityResultLauncher<Intent> launcher) {
if (file == null) return;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("video/*");
if (file.exists()) {
Uri uri = FileProvider.getUriForFile(
context,
"com.haizhen.smartcarhome.fileprovider",
file
);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
launcher.launch(Intent.createChooser(intent, "分享文件"));
}
}
/**
* 分享单个文件(支持 LocalFileNodeBean 类型)
*
* @param context 上下文
* @param fileNodeBean 要分享的文件对象列表(每项包含文件路径等信息)
* @param launcher ActivityResultLauncher(用于启动系统分享)
*/
public static void shareSpecialFile(Context context, LocalFileNodeBean fileNodeBean,
ActivityResultLauncher<Intent> launcher) {
if (fileNodeBean == null) return;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("video/*");
File file = new File(fileNodeBean.getFilePath());
if (file.exists()) {
Uri uri = FileProvider.getUriForFile(
context,
"com.haizhen.smartcarhome.fileprovider",
file
);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
launcher.launch(Intent.createChooser(intent, "分享文件"));
}
}
/**
* 分享多个文件(支持 LocalFileNodeBean 类型)
*
* @param context 上下文
* @param fileList 要分享的文件对象列表(每项包含文件路径等信息)
* @param launcher ActivityResultLauncher,用于处理分享回调
*/
public static void shareSpecialMultipleFiles(Context context,
List<LocalFileNodeBean> fileList,
ActivityResultLauncher<Intent> launcher) {
if (fileList == null || fileList.isEmpty()) return;
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
ArrayList<Uri> uriList = new ArrayList<>();
for (LocalFileNodeBean node : fileList) {
String path = node.getFilePath(); // 你需要确认 getFilePath() 返回的是字符串路径
if (path == null) continue;
File file = new File(path);
if (!file.exists()) continue;
Uri uri = FileProvider.getUriForFile(
context,
"com.haizhen.smartcarhome.fileprovider",
file
);
uriList.add(uri);
}
if (uriList.isEmpty()) return;
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
launcher.launch(Intent.createChooser(intent, "分享文件"));
}
/**
* 保存文件到相册(支持图片或视频)
*
* @param context 上下文
* @param file 要保存的文件
* @param mimeType 文件类型,如 "image/jpeg"、"video/mp4"
* @return 是否保存成功
*/
public static boolean saveFileToAlbum(Context context, File file, String mimeType) {
if (file == null || !file.exists()) return false;
try {
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues();
String relativePath;
Uri collection;
// 判断是图片还是视频
if (mimeType.startsWith("image")) {
relativePath = Environment.DIRECTORY_PICTURES + "/SmartCarHome";
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if (mimeType.startsWith("video")) {
relativePath = Environment.DIRECTORY_MOVIES + "/SmartCarHome";
collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else {
return false;
}
values.put(MediaStore.MediaColumns.DISPLAY_NAME, file.getName());
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath);
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
// 插入媒体库
Uri itemUri = resolver.insert(collection, values);
if (itemUri == null) return false;
// 写入数据
try (OutputStream os = resolver.openOutputStream(itemUri)) {
if (os == null) return false;
try (InputStream is = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
}
}
// 更新为非 pending 状态
values.clear();
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
resolver.update(itemUri, values, null, null);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 批量保存文件(LocalFileNodeBean)到系统相册(支持图片/视频,自动推断类型)
*
* @param context 上下文
* @param fileList 文件信息列表
* @return 成功数量
*/
public static int saveLocalFileListToAlbum(Context context, List<LocalFileNodeBean> fileList) {
if (fileList == null || fileList.isEmpty()) return 0;
int successCount = 0;
for (LocalFileNodeBean node : fileList) {
if (node == null) continue;
File file = new File(node.getFilePath());
if (!file.exists()) continue;
String mimeType = getMimeTypeFromFile(file);
boolean result = saveFileToAlbum(context, file, mimeType);
if (result) successCount++;
}
return successCount;
}
/**
* 通过文件后缀获取 MIME 类型
*/
public static String getMimeTypeFromFile(File file) {
String name = file.getName().toLowerCase();
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg";
if (name.endsWith(".png")) return "image/png";
if (name.endsWith(".mp4")) return "video/mp4";
if (name.endsWith(".avi")) return "video/x-msvideo";
// 可拓展更多格式
return "*/*";
}
/**
* 分享单个文件
*
* @param context 上下文
* @param bitmapFile 要分享的文件
* @param launcher ActivityResultLauncher,用于处理分享回调
*/
public static void shareBitmapFiles(Context context,
File bitmapFile,
ActivityResultLauncher<Intent> launcher) {
if (bitmapFile == null) return;
Uri uri = FileProvider.getUriForFile(
context,
"com.xxx.xxx.fileprovider",
bitmapFile
);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
launcher.launch(Intent.createChooser(intent, "分享文件"));
}
}
LocalFileNodeBean 类
java
public class LocalFileNodeBean implements Comparable<LocalFileNodeBean> {
private String filePath;
private String fileName;
private long fileSize;
private long lastModified;
private EnumUtils.MediaFileInfoType fileType;
public boolean isEditState; //编辑状态
public boolean isSelect; //选中状态
public String progress; //下载进度
public LocalFileNodeBean(File file, EnumUtils.MediaFileInfoType type) {
this.filePath = file.getAbsolutePath();
this.fileName = file.getName();
this.fileSize = file.length();
this.lastModified = file.lastModified();
this.fileType = type;
}
public LocalFileNodeBean(String filePath, String fileName, long fileSize, long lastModified, EnumUtils.MediaFileInfoType type) {
this.filePath = filePath;
this.fileName = fileName;
this.fileSize = fileSize;
this.lastModified = lastModified;
this.fileType = type;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public EnumUtils.MediaFileInfoType getFileType() {
return fileType;
}
public void setFileType(EnumUtils.MediaFileInfoType fileType) {
this.fileType = fileType;
}
public long getLastModified() {
return lastModified;
}
public void setLastModified(long lastModified) {
this.lastModified = lastModified;
}
public boolean isEditState() {
return isEditState;
}
public void setEditState(boolean editState) {
isEditState = editState;
}
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public String getProgress() {
return progress;
}
public void setProgress(String progress) {
this.progress = progress;
}
// --- 支持按时间倒序、名称升序排序 ---
@Override
public int compareTo(LocalFileNodeBean other) {
int result = Long.compare(other.lastModified, this.lastModified); // 倒序:新时间在前
if (result == 0 && !this.fileName.equals(other.fileName)) {
return this.fileName.compareToIgnoreCase(other.getFileName()); // 名称升序
}
return result;
}
}