
需求
- 获取相册中指定文件夹的数据展示出来
- 文件是图片直接展示,文件是视频则生成视频缩略图展示
- 缩略图保存在应用的临时文件夹中
- 拿到的数据根据日期进行分组展示
- 支持删除图片或者视频,并且同步相册
实现步骤
- pubspec.yaml 导包
photo_manager: ^2.8.1
video_thumbnail: 0.4.6
path_provider: ^2.1.1
intl: 0.18.0
- 清单文件AndroidManifest.xml中配置权限
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
- 编码
import 'dart:io';
import 'package:camera/base/base_view.dart';
import 'package:camera/mine/photo_album/photo_album_logic.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../app_allocation/app_style.dart';
import '../../app_allocation/item_constants.dart';
import '../../generated/l10n.dart';
class PhotoAlbumView extends StatelessWidget {
const PhotoAlbumView({super.key});
@override
Widget build(BuildContext context) {
return BaseView<PhotoAlbumLogic>(
model: PhotoAlbumLogic(),
onModelReady: (model) => model.init(),
builder: (context, logic, child) {
return Scaffold(
backgroundColor: AppStyle().getColor.pageBackgroundColor,
appBar: AppBar(
backgroundColor: AppStyle().getColor.mainColor,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Image.asset(
"assets/universal/universal_left.png",
width: 24,
height: 24,
fit: BoxFit.fill,
color: AppStyle().getColor.iconColorWhite,
),
),
Text(
S().photo_album,
style: TextStyle(fontSize: AppStyle().getSize.moderateTitleSize, color: AppStyle().getColor.titleColorReverse),
),
Image.asset(
"assets/universal/universal_edit.png",
width: 24,
height: 24,
fit: BoxFit.fill,
color: AppStyle().getColor.iconColorWhite,
),
],
),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: AppStyle().getColor.mainColor,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: AppStyle().getColor.pageBackgroundColor,
),
),
body: logic.isLoading
? Center(child: CircularProgressIndicator(color: AppStyle().getColor.mainColor))
: Container(
padding: const EdgeInsets.only(left: 15, right: 15, top: 15),
height: ItemConstants.screenHeight,
width: ItemConstants.screenWidth,
child: ListView.separated(
itemCount: logic.dateFileList.length,
separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 20),
itemBuilder: (context, index) {
DateTime dateTime = logic.dateFileList[index].creationTime;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${dateTime.year}-${dateTime.month}-${dateTime.day}",
style: TextStyle(
fontSize: AppStyle().getSize.moderateTitleSize,
color: AppStyle().getColor.titleColor,
),
),
const SizedBox(height: 20),
Wrap(
spacing: 15,
runSpacing: 15,
children: List.generate(
logic.dateFileList[index].dateMediaInfoList.length,
(wrapIndex) {
return Container(
height: 75,
width: (ItemConstants.screenWidth - 15 * 4) / 3,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: FileImage(File(logic.dateFileList[index].dateMediaInfoList[wrapIndex].path)),
fit: BoxFit.cover,
),
),
child: logic.dateFileList[index].dateMediaInfoList[wrapIndex].isVideo
? UnconstrainedBox(
child: Image.asset("assets/camera/play_button.png", width: 24, height: 24, fit: BoxFit.cover),
)
: Container(),
);
},
),
),
],
);
},
),
),
);
},
);
}
}
import 'dart:io';
import 'package:camera/base/base_model.dart';
import 'package:camera/utils/log_utils.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
class PhotoAlbumLogic extends BaseModel {
PhotoAlbumLogic();
final String tag = "PhotoAlbumLogic";
bool isLoading = true;
List<CustomFileInfo> mediaInfoList = [];
List<DateFile> dateFileList = [];
init() {
getMedia();
}
Future<void> getMedia() async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (ps.isAuth) {
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList();
if (albums.isEmpty) return;
List<AssetEntity> assets = await albums[0].getAssetListRange(start: 0, end: 100);
String cameraFilePath = "cameraFile";
Map<DateTime, List<CustomFileInfo>> dateFileMap = {};
for (var media in assets) {
if (media.relativePath!.contains(cameraFilePath)) {
File? mediaFile = await media.file;
String fileUrl = media.type == AssetType.video ? await createVideoThumbnail(mediaFile!.path) : mediaFile!.path;
CustomFileInfo fileInfo = CustomFileInfo(
path: fileUrl,
creationTime: await getFileCreationTime(mediaFile.path),
isVideo: media.type == AssetType.video,
);
mediaInfoList.add(fileInfo);
DateTime date = DateTime(fileInfo.creationTime.year, fileInfo.creationTime.month, fileInfo.creationTime.day);
List<CustomFileInfo> dateMediaInfoList = dateFileMap[date] ?? <CustomFileInfo>[];
dateMediaInfoList.add(fileInfo);
dateFileMap[date] = dateMediaInfoList;
}
}
dateFileList = dateFileMap.entries.map((entry) => DateFile(creationTime: entry.key, dateMediaInfoList: entry.value)).toList();
isLoading = false;
notifyListeners();
Log.d(tag, "getMedia dateFileList:$dateFileList");
} else {
Log.d(tag, "getMedia 没有授权");
}
}
Future<String> createVideoThumbnail(String videoPath) async {
try {
Directory tempDir = await getTemporaryDirectory();
String thumbFileName = "${DateTime.now().millisecondsSinceEpoch}.png";
String thumbPath = "${tempDir.path}/$thumbFileName";
bool thumbnailExists = await File(thumbPath).exists();
if (!thumbnailExists) {
Log.d(tag, "createVideoThumbnail thumbPath:$thumbPath");
await VideoThumbnail.thumbnailFile(video: videoPath, thumbnailPath: thumbPath, imageFormat: ImageFormat.PNG, quality: 100);
}
return thumbPath;
} catch (e) {
Log.d(tag, "视频缩略图生成失败 error:$e");
return "";
}
}
Future<DateTime> getFileCreationTime(String filePath) async {
try {
File file = File(filePath);
FileStat fileStat = await file.stat();
return fileStat.changed;
} catch (e) {
Log.d(tag, "getFileCreationTime error:$e");
return DateTime.now();
}
}
}
class DateFile {
final DateTime creationTime;
final List<CustomFileInfo> dateMediaInfoList;
DateFile({required this.creationTime, required this.dateMediaInfoList});
}
class CustomFileInfo {
final String path;
final DateTime creationTime;
final bool isVideo;
CustomFileInfo({required this.path, required this.creationTime, required this.isVideo});
String formattedDate() {
return DateFormat('yyyy-MM-dd').format(creationTime);
}
}