Flutter 展示相册数据

884 阅读2分钟

image.png

需求

  1. 获取相册中指定文件夹的数据展示出来
  2. 文件是图片直接展示,文件是视频则生成视频缩略图展示
  3. 缩略图保存在应用的临时文件夹中
  4. 拿到的数据根据日期进行分组展示
  5. 支持删除图片或者视频,并且同步相册

实现步骤

  1. pubspec.yaml 导包

# 获取相册文件
photo_manager: ^2.8.1

# 生成视频缩略图
video_thumbnail: 0.4.6

# 获取临时文件夹路径
path_provider: ^2.1.1

# 解析时间
intl: 0.18.0

  1. 清单文件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" />
  1. 编码
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';

//相册页View
//
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';

//相册页logic
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 来将媒体文件按日期分组
      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>[];
          // 将fileInfo附加到列表
          dateMediaInfoList.add(fileInfo);
          dateFileMap[date] = dateMediaInfoList;
        }
      }
      // 将分组后的数据保存到 dateFileList 中
      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);
  }
}